# maxclicks documentation: full text > maxclicks is an agentic customer engagement platform. Give maxinja a goal to work on customer data, audiences, personal emails and workflows in your space. These docs explain the service for marketing teams and the public API for developers. Start with the customer outcome. Connect the relevant data and brand context, ask the agent to build the audience or email, and review the result before sending. Adaptyle writes selected email content for each recipient. Use the email guides to configure senders, personalize messages, and manage delivery. Product guides describe dashboard capabilities. API reference pages describe the operations available programmatically. # Email marketing automation documentation Source: https://docs.maxclicks.ai/ maxclicks documentation # Build your first personal campaign. Give maxinja the brief. Use customer data to personalize the message. Review the result before sending. ## One email. Three different customers. Loopwell wants new signups to create their first project. Maya is planning a launch; Leila has interview notes to organize; Hugo is preparing a client handoff. Adaptyle uses those facts to write a different suggestion inside the same email. The instruction belongs in the editor. Recipients see a finished email with the same design and call to action. These saved previews use fictional customer data; your own generations will depend on the context you supply. ## If you work in the app Start with a customer action you want to encourage. Each walkthrough connects the data, audience and message to that action. | Your goal | Start here | | ----------------------------------------- | --------------------------------------------------------------------------------------------------------- | | Help new signups start using your product | [Build an onboarding journey](/guides/workflows) that checks progress before sending a reminder. | | Encourage a second purchase | [Build a repeat-purchase campaign](/guides/repeat-purchase) using order history and customer preferences. | | Learn how personal messages are made | [Create your first personalized email](/guides/first-personalized-email) and compare three recipients. | | Launch a reviewed campaign | [Send your first email](/quickstart/send-your-first-email), from sender setup to delivery results. | ## Core concepts maxinja builds the campaign. Evaluated attributes make related customer data reusable. Workflows decide when to act. Adaptyle writes the marked passages for each recipient. Unsure which part to use? [Choose between maxinja, Adaptyle, evaluated attributes and workflow AI](/guides/choose-your-tools). For the foundations, follow [maxclicks 101](/101). ## If you build on maxclicks Connect the records and events your campaigns need. The [developer quickstart](/quickstart) takes you through authentication, a customer record and sending a saved template. The [API reference](/api) covers requests, responses and retry behavior. Use [MCP, the CLI and agent skills](/ai) to work from your development tools. For a sending issue, start with [delivery checks](/domain); for usage, see [billing](/billing). [Connect your product](/quickstart) --- # Welcome to maxclicks 101 Source: https://docs.maxclicks.ai/101 # Welcome to maxclicks 101 maxclicks brings customer data, AI agents and email into one workspace. You give maxinja the campaign or journey brief; it builds from the records, audience definitions and brand guidance you supply. Your team can inspect the resulting audience, email and workflow before putting them into use. Use this course to understand how the service fits your marketing operation, what your team controls, and where a developer connects your source systems. Each lesson explains the decisions behind a feature before the setup steps. ## What maxclicks does A customer record becomes useful when it changes who you contact, what you say or when you act. Four connected capabilities make that possible: * **You decide what you track.** Set up [schemas](/schemas) for your contacts, for the things around them like products and orders, and for the events they trigger. A field is either saved on the record or worked out on demand. * **You direct the agents.** Describe an audience, calculation, campaign or journey. maxinja generates the definitions and content in their supported app surfaces; inspect the saved result and refine the brief. * **Customer context changes the message.** [Liquid](/liquid) inserts known values; [Adaptyle](/adaptyle) writes the passages you mark using selected data and brand context. Keep essential prices, dates and links in fixed content. * **Automation reacts to your data.** [Workflows](/workflows) run when an event fires or a record changes. Your team builds campaigns and journeys in the app. Developers use the [REST API](/quickstart) to maintain records, supply events, trigger supported sending operations and read results. Email is the available communication channel. ## From customer context to campaign Consider a coffee business introducing a new single-origin coffee. Purchase records can identify returning customers. A saved segment selects those with at least two orders and no recent purchase. The email keeps the product, price and destination link fixed while Adaptyle adapts the introduction for a pour-over customer, an espresso customer or someone buying a gift. The work remains inspectable: source records, matching contacts, selected fields, generated samples and delivery results. The [repeat-purchase walkthrough](/guides/repeat-purchase) follows this campaign from its order data to recipient review. ## Two ideas to get first **You shape the data.** A schema is like a form, an attribute is a question on that form, and a record is one filled-out copy. If a spreadsheet is a clearer picture: a schema is a table, an attribute is a column, and a record is a row. You define the questions once, and every contact, product, or order is another copy. **You describe conditions instead of writing code.** When you want a segment like "customers who bought in the last 30 days but haven't opened an email since," you write that sentence. maxinja produces a human-readable description you review, plus a hidden expression that runs it. You read English, not SQL. ### maxinja is part of the product Segment conditions, evaluated attributes, broadcast audiences, and workflow logic are all generated from plain language. Review the generated description, then check it against matching and excluded records before using it. That second idea gives you the **evaluated attribute**: a field worked out when you need it, like "the company this contact works for" or "total spend last quarter." You write the sentence, maxinja builds the field. The [data model](/101/data-model) lesson covers this in depth. ## The lessons [Working with maxinja](/101/maxinja): Turn a campaign brief into saved work you can inspect. [Navigating your space](/101/navigating): Where everything lives and how to move around. [The data model](/101/data-model): Schemas, attributes, records, and evaluated attributes. [Importing data](/101/importing-data): Bring your existing contacts and records into maxclicks. [Segments](/101/segments): Describe an audience and let maxinja build the filter. [Designing email](/101/email): Build and personalize email content in the editor. [Sending](/101/sending): Send broadcasts and templates to the right people. [Workflows](/101/workflows): Automate multi-step processes with triggers and actions. [Deliverability](/101/deliverability): Authenticate your domain, manage consent and interpret delivery signals. [Your account](/101/account): Manage your space, members, settings, and billing. [For developers](/101/developers): The REST API, integrations, and building on maxclicks. [Glossary](/101/glossary): Quick definitions for every term in the course. ## How to work through it Start with maxinja and the work it can build. Then learn the workspace, data, audience, email and journey that support your campaign. Each one explains why before how, and links to its full [reference](/) page when you want the detail. Read them in order the first time, then come back to whichever one you need. ## Next Meet maxinja and learn how to brief it, inspect its saved work and refine a campaign. [Meet maxinja](/101/maxinja) --- # Your space, members and billing Source: https://docs.maxclicks.ai/101/account # Your space, members and billing A workspace gives your team a shared customer model, campaigns, brand and billing. Each **space** is isolated from the others you belong to. Use this lesson to understand ownership, member access and the costs attached to your operation. A space has a name, an avatar, and a short unique slug that appears in the app's URL. Admins manage those, along with domains, senders, members, and billing, in **Settings**. ## Members and roles A person joins a space as a **member**. The membership carries permissions that decide what they can do, and you assign them as one of three fixed roles. | Role | Permissions | Grants | | -------------- | ------------------------ | ------------------------------------------------------------------------------------------------------- | | Member | `READ`, `WRITE` | Create and edit campaigns and customer data, and run day-to-day actions such as suppressing an address. | | Non-paid Admin | `READ`, `ADMIN` | Manage workspace identity, members, billing and integrations without editing access. | | Admin | `READ`, `WRITE`, `ADMIN` | Member permissions plus workspace administration. | You pick the role when you add a member, and you change it from the members table. Permissions outside these three combinations are not assignable. You add members by email: existing users join at once, unknown emails receive an invitation link. ### Team access is included Collaborators and maxinja access have no per-seat charge. Workspace roles still control what a teammate can do. Paid AI work uses the shared workspace wallet; there is no monthly AI allowance. ## Free and Premium plans Each space is billed on its own, on one of two plans. * **Free**: no subscription, with 100 recipient sends per month as a hard cap. AI credits are not included; admins can buy them with a saved card. * **Premium**: a $49 monthly workspace subscription with 5,000 records and 50,000 recipient sends. Extra 1,000-record packs cost $5/month and include 10,000 extra sends for a full month. AI is paid separately. A **record** is a contact or an object, and both count toward the 5,000. On Premium, records past the included 5,000 add record packs, emails past the included allowance are billed per email, and AI generation, personalization, and workflow runs draw from credits. Free has unlimited records and can buy more credits, but its 100-email monthly cap remains fixed. ## Credits meter your usage **Credits** are how maxclicks charges for usage. One credit is one unit of the workspace billing currency, held in its wallet. All collaborators and background work use the same prepaid balance; no monthly AI allowance or gift is included. AI generation, personalization, metered tools and email overages consume credits; included emails have no per-recipient send fee. The **Usage** report in Billing shows where your credits went. When no credits are available, new operations may be refused while workflow and broadcast execution can wait for funding. A terminal failure is different and needs investigation. Premium has a limited negative-credit buffer and optional **auto reload**, so the displayed balance is not an emergency stop control. Use workflow pause or broadcast stop when you intend to prevent more execution. Teammate and background AI work share the workspace wallet. Read the email **Estimate** before scaling a campaign, including corrections and reviews as well as generation. Unknown estimates are not zero cost. ### Free plan stops at its email cap On Free, sends also hard-stop when you reach 0 remaining emails for the month. The cap resets monthly; purchased credits carry forward. ## Next See [How billing works](/billing) for subscription prices and an example bill. [AI and usage credits](/billing/credits) explains which balance pays for your work and how to top it up. [Billing and payments](/billing) --- # The data model Source: https://docs.maxclicks.ai/101/data-model # The data model Customer context starts with a model of the business. To select customers who bought twice this month, maxclicks needs customer records, purchase records and the fields that connect them. Define that model and supply the data once, then reuse it across audiences, email and journeys. Three words carry the whole thing: * A **schema** is a kind of record: Customer, Product, PurchaseCompleted. * An **attribute** is a field on that schema: email, price, order total. * A **record** is one entry: one customer, one product, one purchase. For example, **Account** is an object schema; **plan** is one of its attributes; **Studio North, on Pro** is one Account record. Maya and Daniel are two Contact records who can both refer to that same account. The model should let you answer a concrete question: “Which people administer a Pro account?” The account contributes its plan, each person contributes their role, and the final audience contains people. ## Decide what kind of thing you are describing Every schema is one of three types, and the type is fixed once you create it. | Type | Use it for | Examples | | ------- | ------------------------------------------ | -------------------------------- | | Contact | People you can email | Customer, Subscriber, Student | | Object | Things you track but never email | Product, Order, Company | | Event | Something that happened at a point in time | PurchaseCompleted, CartAbandoned | Contacts and objects are records you create, update, and keep. Events record occurrences: a PurchaseCompleted event says an order was paid at a particular time; the Order object can later change to refunded. A ready event can start a configured [workflow](/101/workflows). Use an object for something with its own identity or repeated records: one shopper can have many orders, and many people can share one account. Keep a simple personal fact, such as preferred language, on the contact. Store matching identifiers and define an [evaluated relationship](/101/data-model/relationships) so maxclicks knows which things belong to which people. Start with one contact schema. Add objects for the state your messages need and business events for occurrences your journeys use. The Events area also supports typed [collected events](/events#collected-events) from browser or server sources, including observations that build customer measures without triggering an email for every occurrence. ## Where you set this up * **Contact and object schemas** live in Settings, under Schemas. * **Event schemas** live in Settings, under Events. * **The records themselves** get their own page under Records in the top navigation, one entry per contact or object schema. From a record page, the icons beside the title jump straight to Schema settings and Manage attributes for that schema. ## Fields you fill in, and fields maxclicks works out There are three useful ways to supply a field: | Value source | Renewal-campaign example | When it changes | | -------------------- | --------------------------------------------------------- | ------------------------------------------------------------ | | Stored | Subscription renewal date supplied by your billing system | A record write; a missing-value policy can fill a new record | | Evaluated | The subscription matching this customer's subscription ID | When a consumer requests the relationship | | Scheduled evaluation | Customer activity count in the last 30 days | A scheduled background calculation publishes a result | Evaluated and scheduled evaluated fields both require [expansion](/attribute-expansion) for email data, but they have different freshness. A computed count with no qualifying history is empty, not zero. An old publication can remain inspectable while refresh is delayed; an enabled field with no complete or fresh publication blocks a decision that requests it. A live relationship can only see records already supplied to maxclicks. Do not add every possible field before your first campaign. Write down the actual message and audience rule, then identify the facts, joins and missing-value behavior needed to support them. ## Get the shape right before you load data maxclicks validates supported record fields against their schema. Unknown custom fields can be omitted, and the standard CSV importer converts some cell types before validation. Declare the fields first, then test a small set of records and inspect the values that actually arrived. A successful write proves acceptance of that input, not that the source mapped every intended field correctly. A schema's type and slug, and an attribute's key, stay fixed. A stored attribute's value type and accepted values can change through setup review. Choose how existing values are converted, inspect affected rules and apply the reviewed change. See [changing attributes](/attributes#what-you-can-change-later-and-what-you-cannot). One more rule to know early: once you create your first contact schema, your space always keeps at least one. You cannot delete the last one. ## The rest of this lesson Five short pages, in the order you will need them: 1. [Understanding schemas](/101/data-model/schemas): create one, and see the fields you get for free. 2. [Understanding attributes](/101/data-model/attributes): add your own fields and pick their types. 3. [Understanding records](/101/data-model/records): get records in without creating duplicates. 4. [Understanding events](/101/data-model/events): the records you fire instead of edit. 5. [Connecting records](/101/data-model/relationships): link contacts to everything else. ## Next Start with the foundation: how to create a schema and the fields it comes with. [Understanding schemas](/101/data-model/schemas) --- # Understanding attributes Source: https://docs.maxclicks.ai/101/data-model/attributes # Understanding attributes Attributes define the customer facts a campaign can use: email address, plan tier, renewal date or company relationship. Each belongs to a [schema](/101/data-model/schemas). Choose fields for the decisions you need to make, with clear names, types and descriptions. ## Add a field Open **Settings → Schemas**, pick the schema, then **Attributes → Create attribute**. Set the source and field details: * **How is the value set?** Choose **Stored value** or **Evaluated attribute**. For a stored value, **Value type** defines the allowed format. * **Name.** The label you will see everywhere in the app. "Plan tier". * **Key.** How the field is addressed in imports, the API, and personalization. Suggested from the name, and **permanent once saved**. * **Description.** Optional, and worth writing anyway: maxinja reads it when it builds segments and evaluated attributes. The Attributes tab lists your system fields first (marked **System**, not editable) and your own fields underneath. ## Pick the type that describes the value Choosing the most specific type is how you keep data clean, because maxclicks validates every incoming value against it. A field typed as Email rejects "not an address" before it ever reaches a record. | Type | Holds | | ------------------- | ---------------------------------------------------------------------------------------------------- | | Text | Any text. Can be multi-line, or restricted to a set of options shown as a dropdown or radio buttons. | | Email | An email address, checked and normalized. | | Phone | A phone number, checked and normalized. | | URL | A link, marked as a web page, a file, or an image, with an option to show a preview. | | Number | Optionally integer only, with a minimum, a maximum, or shown as a percentage. | | Decimal | Precise numbers with an optional currency. Use this for money. | | Checkbox | True or false. | | Timestamp | A date and a time. | | Date | A calendar date, no time. | | Time | A time of day, no date. | | Multi-select | A list of values, optionally from fixed options, shown as checkboxes or a dropdown. | | Address | A postal address. | | Geolocation | A coordinate pair. | | JSON | A structure you describe yourself, for anything the other types do not cover. | | Evaluated attribute | Generated logic supplies an on-demand answer or scheduled event-history measure. Review its timing. | ## Decide what happens when the value is missing The form asks **When a record is created without a value**. Choose one policy: * **Leave it empty** for an optional fact you may not know. * **Use a default value** for a genuine fixed fallback. * **Require it** when a missing value should reject the new record. * **AI autofill** to **Infer from the record** or **Follow a prompt**. The policy applies to creation, not an existing record's updates. It does not backfill older records, regenerate an omitted field on an update, or overwrite a value you supply. An explicit empty value still has to satisfy the field's allowed emptiness. For example, a fixed `sourceCampaign` default can label new newsletter signups. Reimporting an existing subscriber with that column blank will not apply the default again. AI autofill is paid work for each new record and missing AI field that needs it; test it on a small sample before a large import. ## Fields maxclicks works out for you Choose **Evaluated attribute** under **How is the value set?** and the form changes: instead of configuring a value, you write a prompt describing what the field should compute. ```text Sum the amount of completed Orders whose customerId matches this Customer's userId and whose purchasedAt is within the last 30 days. Use EUR orders only. Return zero when there are no matching orders. ``` This example needs an Order object schema with those fields. It returns an on-demand total from order records. Decide separately what should happen when the customer has no usable identifier or the query fails; neither proves they spent zero. Type `@` in the prompt to reference this record's own fields. maxinja writes the logic, decides whether the answer is a value or a set of related records, and shows you a plain-language description of what it built. Read that description before you save: it is the part you are responsible for checking, and you can refine the prompt until it says what you meant. The evaluated value is not stored. maxclicks computes it when requested, using the source records currently available to the workspace. Generating the definition spends credits once; working the value out afterwards is a database query, not an AI call. [Connecting records](/101/data-model/relationships) covers what these fields can reach and how to use them. For a rolling measurement of event history, create an **Evaluated attribute** on the destination contact or object schema. Choose a destination contact or object schema, describe a count, sum or latest value over one source, and review the generated window and cadence. The default proposal uses 30 days and hourly updates. A preview does not publish the value; wait for **Published value** and check **Data through**. A valid publication with no matching event history yields an empty value rather than zero. No complete publication or an expired enabled result blocks a new decision that requests it. That matters for an audience such as “customers with no observed purchases.” See [scheduled evaluated attributes](/attributes#computed-attributes) for source links, supported windows and publication states. ## What you can change afterwards Open an existing attribute to edit its name and description. The key stays fixed, but a stored field's value type and validation rules can be changed through setup review. Choose an **Existing records** policy: preserve values, fill empty ones, map exact values, convert to the new format, or replace all values. Review the resulting values and saved consumers in **Changes** before applying a change that needs your decision. A renamed label does not update the key used by imports and templates. See [attribute changes](/attributes#what-you-can-change-later-and-what-you-cannot). ## Before you delete a field maxclicks refuses to delete an attribute that is still in use, and tells you which kind of thing is using it: another calculated attribute, a segment, a broadcast, a workflow step, a webhook, an email template, or a form. Clear those uses first. Deleting a field that nothing depends on still means the values in it are gone from every record. For a populated schema, follow [Setup changes](/setup-changes) to inspect converted values, affected campaigns and application status. ## Next See how base and custom fields come together in one record in [understanding records](/101/data-model/records), or open the [attributes reference](/attributes) for every type and its settings. --- # Understanding events Source: https://docs.maxclicks.ai/101/data-model/events # Understanding events An event records a business moment that can change the next customer action: a completed purchase, an abandoned cart or an expired trial. Declare the facts it carries, then use it to start a relevant journey or inform a customer calculation. ## Define the event before anything sends one For a business occurrence, open **Settings → Events → Create event schema**. Use a clear name such as PurchaseCompleted, then declare its custom fields in **Attributes**: amount, currency, order ID and the status your campaign should count. Have your producer supply a stable `eventId`, the occurrence time, and a maxclicks `contactId` or `objectId` when the event describes a known record. At most one explicit subject is allowed. A custom field containing an email does not replace that subject link. For browser or server observations, bind a [collection source](/events#collected-events) on the same event schema's **Collection** tab. Select the associated contact or object schema and an optional JSON context attribute. The event's attributes and slug define what this producer sends; there is no separate collection property definition to keep in sync. ## Events are added, never edited An accepted business occurrence keeps the definition and input associated with that acceptance. It is not updated like a contact or order. If a purchase is refunded later, record the refund as a separate occurrence rather than changing the original purchase event. Preparation can still complete after acceptance. Missing AI-autofill fields can leave an event pending until the values are prepared. Only ready events feed normal business-event queries and downstream event delivery. Erasure of a subject or schema can remove associated history; “immutable” does not mean exempt from deletion. ## Make a retry safe Use one stable `eventId` for one occurrence and repeat that ID with unchanged input after a network timeout. An identical retry returns the original acceptance receipt with a duplicate marker. Reusing the ID with different input is a conflict. The original receipt is not a live status report. If it says the event was pending at acceptance, use the event status operation to learn whether preparation is now ready, failed or unresolved. Creating a different ID to retry creates a different occurrence and can create more automation. ## Put an event to work An event on its own is only a note that something happened. It becomes useful when you point automation at it: * **Start a workflow.** Add an **Event Fired** trigger step, choose the event schema, and every matching event starts a run with the event's data in hand. A `PurchaseCompleted` might start a workflow that waits three days and then asks for a review. * **Call your own systems.** A [webhook](/webhooks) with the **Event fired** trigger forwards it to a URL you control. ## Choose the stream for the decision Business events and collected observations serve different decisions. A completed purchase can start a journey immediately after it is ready. Product views or feature usage can form a typed collected stream and feed a recurring measure of interest or activity. A browser source collects anonymous observations. Your authenticated server controls the link to a contact. That link does not grant email consent, and saving a collection definition does not publish a workflow for it. A scheduled evaluated attribute can count observations over a window and refresh on a cadence. It may be more useful to act when the resulting measure crosses a threshold than to send a message for each observation. See [scheduled evaluated attributes](/attributes#computed-attributes). ## Record or event? Ask whether you are describing a thing or a moment. If you would ever change it later, it is a record. If it only makes sense as a one-time signal, it is an event. | Use a record when | Use an event when | | --------------------------------- | ----------------------------------------------- | | It is a thing you keep and update | It is a moment that happened at a point in time | | It answers "what is true now?" | It answers "what just happened?" | | Example: a Customer, a Product | Example: PurchaseCompleted, CartAbandoned | The two work together. Sarah remains the Customer contact; Order 184 is the object you keep updating; PurchaseCompleted is the occurrence that can start her thank-you journey. Supply the explicit contact subject when the event should contribute to Sarah's scheduled purchase history, then configure and publish the workflow that acts on it. ## Check what actually arrived Inspect the event library and **Logs** history for a business schema, then check the individual acceptance/readiness status when diagnosing a missing event. **Live** in the library means recent activity and a configured trigger, not proof that this occurrence delivered an email. Next inspect the published trigger, its condition and the workflow run. For a scheduled evaluated attribute, check its published **Data through** timestamp: refreshing the event list does not refresh the calculation. ## Next [Connecting records](/101/data-model/relationships), or the [events reference](/events) for sending them from your own code. --- # Understanding records Source: https://docs.maxclicks.ai/101/data-model/records # Understanding records A record is one customer, order, product or other entry in your business model. Its [schema](/101/data-model/schemas) defines the fields; the supplied values give an audience or email something concrete to use. This lesson covers adding, inspecting and updating those records. ## Add a record Open Records in the top navigation and pick the schema you want. From there: * **One at a time.** Contact schemas have an "Add" button named after the schema (Add Customer), object schemas have a "New" one (New Product). You get a form built from that schema's fields. * **In bulk.** Contact schemas have an Import button on the same row. Every schema also has an Import tab under Settings, then Schemas. Both take a CSV. See [getting your data in](/101/importing-data). * **From your systems.** Records also arrive through the API, from a [form](/forms) submission, from an [integration](/integration), or from a workflow step. Whichever route they take, they go through the same checks. ## Contacts and objects A record represents one specific person or thing. Sarah is one Customer contact. Her four orders are four Order objects, each with its own identity, amount and status. Updating one order should not replace the other three or create another Sarah. | Record type | What it is | Examples | | ----------- | ------------------------------------------- | ----------------------------------- | | Contact | A person you can send email to | Customer, Subscriber, Student, Lead | | Object | A business thing you track but do not email | Product, Order, Company, Course | A contact carries extra fields an object does not need: consent, subscriptions, and email statistics. An object never receives an email, but knowing your products and orders is how you personalize the email you send to people. Events, the third type, work differently and have [their own page](/101/data-model/events). ## How maxclicks keeps duplicates out Import and upsert operations match within the selected schema. Contacts use a platform ID when supplied. Otherwise `userId` takes priority, email is used only without a user ID, and phone only without either. Objects use platform ID or `externalId`. This is a priority rule, not a search for any field that happens to match. Sending a new user ID alongside an email can describe a different profile from an existing email-only contact. Without a usable identity, repeated input can create repeated records. Keep your source identifier stable: use `userId` for people and `externalId` for objects. Select the intended API operation too: explicit create, update and upsert have different conflict outcomes. [Contacts](/contacts) and [Objects](/objects) explain those distinctions. ## Sending a field also updates it Because a write is both a lookup and an update, referencing a record can change it. Sending a template to `ada@example.com` with a first name of "Ada" attached does more than address the email: it finds that contact and writes "Ada" into their first name. That is usually what you want, and it is the reason you never have to send a whole record to reference one. An email address on its own is enough to find or create a contact. It is also worth remembering when a system of yours holds stale values: whatever it sends, it also saves. ## When an identifier is already taken Sometimes a write hands a record an identifier that already belongs to a different record. Nothing is silently merged. Depending on the route, maxclicks either applies everything that does not clash and reports the one change it skipped, or refuses the write and changes nothing. Resolve the source mapping and inspect the affected profiles; maxclicks does not automatically merge their identity, history or consent for you. Which route does which is worth knowing before you write a sync, and [contacts](/contacts) spells it out. ## Fields you cannot set Some fields are maintained by maxclicks: creation/update timestamps, contact source and email statistics. Evaluated relationships and scheduled evaluated attributes are also read-only outputs, not values an import or record write can set. Ordinary public record responses do not include those expanded results. Subscriptions and communication topics are not in that list. They can be set on any write, which means an import or a sync can subscribe people. Treat those two as consent, and make sure whatever writes them is actually recording a choice a person made. The [contacts reference](/contacts) covers consent, suppressions, and the audit trail. ## Next [Understanding events](/101/data-model/events): the records you fire instead of edit. --- # Connecting records Source: https://docs.maxclicks.ai/101/data-model/relationships # Connecting records Connect a customer to the order, account or subscription that explains the message you want to send. Relationships make that context available beyond the contact profile, so a campaign can use the relevant purchase or renewal details. You connect them with an evaluated field: the **Evaluated attribute** source choice. You describe what you want in plain language, maxinja works out the logic, and the value is calculated fresh every time something asks for it. ## Start with the values that connect In this illustrative setup, Customer has a custom text field `accountId`. Account has the built-in `externalId` plus custom `accountName` and `plan` fields. * **Maya:** a Customer with `accountId` set to `studio-north` and her own email and preferences. * **Daniel:** another Customer with the same `accountId` and his own email and preferences. * **Studio North:** one Account with `externalId` set to `studio-north` and `plan` set to `Pro`. Both people should resolve to one shared account. The stored ID is a value; the evaluated definition below is the rule that turns it into a related record. If either side is missing or uses a different identifier, the lookup will not find the intended account. ## Create one Open Settings, then Schemas, pick the schema, then the Attributes tab, then Create attribute. Choose **Evaluated attribute** under **How is the value set?**, name the field, and write a prompt: ```text Return the Account whose externalId equals this Customer's accountId. Return one account, or no record when there is no match. ``` Type `@` in the prompt to reference this record's own fields. maxinja decides on its own whether the answer is a value or one or more related records, and shows you a plain-language description of what it built. Review the target schema and the **one** or **many** cardinality as well as the description. Specify the actual join fields, the no-match behavior and, for lists, order and limit. Test one known match and one no-match record before using the relationship. ## One field type, three jobs Links, calculations, and lookups are all the same attribute here: | What you want | What you write | | ------------------------ | ----------------------------------------------------- | | A link to another record | "The company this contact works for" | | A calculation | "Total spent on completed orders in the last 30 days" | | A lookup | "Their most recent order" | A relationship can start on an object too. An Order can resolve its Product through a shared product identifier, and a contact's Recent orders can expose those products when you select the nested relationship. Define each link and its match keys explicitly. For a shopper with several orders, create **Recent orders** on Customer and ask for completed Orders whose `customerId` equals Customer's `userId`, newest first by `purchasedAt`, limited to three. Check **many** cardinality. A customer with four qualifying orders should return the latest three, and a customer with none should return an empty list. If one person belongs to several accounts and has a different role in each, a single `accountRole` on the person is insufficient. You can model each membership as an Object with the person's identifier, account identifier and role, then define evaluated lookups through those membership records. This is your data model; maxclicks does not add membership records automatically. ## Read the source at the right time An evaluated relationship resolves the available source record when requested. Updating the account name changes a later lookup without copying that name onto every customer. It does not make an upstream sync more current than it is. A scheduled evaluated attribute is different: it reads a published event calculation with a measured window and freshness. An older publication can remain visible for inspection, but an enabled unavailable or expired result blocks a new decision that requests it. A paused definition with a retained publication can use that last result. Use the [attribute comparison](/evaluated-attributes#stored-or-evaluated) when deciding which kind of field your campaign needs. ## Where the connected data shows up An evaluated relationship runs when a consumer requests it. Select only the relationships and nested fields that consumer needs. In the email editor, the **Data** panel lists the fields available to your content. Stored fields are always there. Evaluated and scheduled evaluated fields have a checkbox: select an evaluated relationship to run its lookup, or select a scheduled evaluated attribute to read its published value. Your copy can then reference the selected result. The same choice exists on workflow steps that read a record. The choice belongs to the email or the step, not the record. The same contact can go into two different emails and each one pulls in different connected data. A later evaluated lookup can see a newly supplied order. Scheduled evaluated attributes used by an active broadcast revision or workflow root run retain their selected published generation; a current profile preview can therefore differ from that run. Generating the field spends credits once, when maxinja writes it. Working the value out afterwards is a database query rather than an AI call, so using it in a large broadcast does not multiply your AI costs. ## Keep the chain pointing one way An evaluated field can depend on another evaluated field, which makes nested lookups possible. It also means you can build a loop: field A defined in terms of B, and B defined in terms of A. Nothing stops you at the point you save it, and neither value can ever be worked out afterwards. Sketch the dependencies before nesting them. A relationship in each direction can be useful, but do not define a field in terms of a chain that requires itself. Expansion detects a circular dependency and fails. ## Next Go deeper in the [evaluated attributes reference](/evaluated-attributes), or get your records in with [getting your data in](/101/importing-data). --- # Understanding schemas Source: https://docs.maxclicks.ai/101/data-model/schemas # Understanding schemas A schema defines one part of your customer model, such as Customer, Product or Subscription. Agree on its fields before importing records or connecting a source system: maxclicks saves the properties the schema declares. ## Plan one person and one related thing For an account-renewal email, start with Customer as **Contact** and Account as **Object**. Customer holds the person's name, email and preferences. Account holds the account name, plan and renewal date. Several Customer records can refer to the same Account. For a store, use Customer and Order instead: one shopper can have several orders, each with its own amount and status. A schema defines the fields for all those orders; each imported row supplies one actual order. You will connect the records in [Connecting records](/101/data-model/relationships). ## Create a schema Open Settings, then Schemas, then Create schema. You fill in five things: | Field | What to put in it | | ----------- | ------------------------------------------------------------------------------------------------------- | | Type | Contact for people you can email, Object for everything else. Fixed once created. | | Name | The singular name, without spaces. `Customer`, `Product`. | | Plural name | Suggested for you from the name. `Customers`, `Products`. | | Slug | The URL-safe form, also suggested. This is how the schema is addressed from the API. | | Description | Optional, but worth writing. maxinja reads it when it builds segments and evaluated attributes for you. | Event schemas are created in a different place, under Settings, then Events, because they behave differently. They get their own lesson in [understanding events](/101/data-model/events). Once the schema exists, it appears under Records in the top navigation and gets three tabs of its own: Configuration, Attributes, and Import. ### Names that are already taken Names are unique inside your space, spaces are stripped out of them, and a handful of words are reserved because they name built-in types: `string`, `number`, `boolean`, `date`, `email`, `phone`, `url`, `json`, `object`, `event`, and similar. The app tells you while you type if a name is unavailable. ## The fields you get before you add anything Every schema ships with fields you do not create and cannot remove. They show up in the Attributes tab marked **System**. **Contact:** Email Address, Phone Number, First Name, Last Name, Full Name, Avatar URL, User ID, Notes, Tags, Subscriptions, Topic IDs, plus ID, Source, Email Statistics, Created At, and Updated At which maxclicks maintains for you. **Object:** ID, External ID, Notes, Tags, Created At, Updated At. **Business event:** ID, Unique Event ID, occurrence and receipt timestamps, readiness, accepted definition revision, and an optional explicit contact or object subject. [Events](/events) explains why acceptance and readiness are separate. Contacts carry the extra consent and email fields because they are the ones you message. Objects are the supporting cast: they never receive an email, but knowing your products and orders is how you personalize the email that people do receive. Everything past this list is yours to add, on the Attributes tab. See [understanding attributes](/101/data-model/attributes). ## Only the fields you declared are saved Declare a custom field before importing or sending it. Unknown fields can be omitted, and a value that cannot satisfy its declared type is rejected. Some input routes perform conversions first, so inspect the resulting records as well as the success count. For a business event, use the explicit `contactId` or `objectId` field to identify its subject. A custom `customerEmail` property may be useful context, but it does not automatically create that relationship. Scheduled event measures need the explicit subject link. After setting up Customer and Subscription, create one of each with matching source identifiers. Test the evaluated relationship before loading the whole dataset. The schema tells maxinja which fields exist; your sample check establishes that the values connect in the way you intended. ## What you cannot change later You can rename a schema and rewrite its description any time. Three things are fixed: * **The type.** A contact schema never becomes an object schema. * **The slug.** * **Any attribute's key and type**, once that attribute exists. Segments, evaluated attributes and generated conditions refer to these stable names and types. If the intended shape changes, create the replacement field or schema, supply its data and update the consuming definitions. ## Deleting a schema Deleting a schema permanently removes its records and can affect related event and derived data. The app asks you to confirm and follows a deletion operation that may continue in the background. Wait for completion; other workspace processing can be gated while cleanup runs. maxclicks blocks the delete outright while anything still depends on the schema: another schema's evaluated relationship pointing at it, or a segment, broadcast, workflow step, or webhook that references it. Remove those first. It also refuses to delete your last contact schema, because a space always keeps one. ## Next Attributes are the questions on the form. Continue to [understanding attributes](/101/data-model/attributes), or open the [schemas reference](/schemas) for the full detail. --- # Deliverability basics Source: https://docs.maxclicks.ai/101/deliverability # Deliverability basics A campaign needs a verified sending identity, an eligible audience and a healthy sending history. This lesson explains the controls maxclicks provides for authentication, consent and suppressions, and how to read delivery signals without treating acceptance as inbox placement. For a first campaign, work through three checks in order: can your sender send, may this customer receive this message, and what happened to the delivery? A verified domain answers the first. A subscribed, unsuppressed contact answers part of the second. Only the resulting email activity can tell you what happened after the attempt. ## Verify a sending domain maxclicks sends on your behalf, so inbox providers need proof you authorized it. You provide that proof by verifying a domain: you add DNS records that say maxclicks may send as your domain. Verification is required. Nothing sends from a domain until it is verified. There is no shared fallback domain. The first domain becomes the space default when added. That default still cannot send until verification succeeds. Moving the default later requires a verified domain, and existing emails can retain an explicit sender. The flow is short: ### Register the domainAdd your domain, for example `company.com`, under Settings. ### Add the recordsCopy the DNS records maxclicks generates and add them at your DNS provider. ### Start verificationFollow the background verification results until the domain shows **Verified For Sending**. DNS propagation and provider processing can delay the result. The same job re-checks a verified domain. If its DNS records disappear, the job un-verifies the domain, and every send from it stops. Leave the records published as long as you send from the domain. ## DNS records: SPF, DKIM, DMARC, return path maxclicks generates the exact records to add. Each one plays one part in the authentication story. | Record | What it proves | | ----------- | ----------------------------------------------------------------------------------------------- | | DKIM | Your messages were cryptographically signed by your domain. Receivers can verify the signature. | | SPF | maxclicks is allowed to send for your domain. | | DMARC | How receivers should treat mail that fails the checks above. | | Return path | A subdomain where bounce notifications go, so failed deliveries are handled. | You do not memorize the syntax. maxclicks shows each record with its value, and the [domain](/domain) page explains where each goes. The return-path subdomain is created for you beneath your chosen domain. ## Suppressions A suppression is a do-not-contact mark on an email address. It applies to every contact that shares the address, including contacts created later. A suppression applies either to your space alone or across all of maxclicks. | Reason | Set by | | ------------------------- | ------------------------------------------------- | | `unsubscribed` | contact | | `hard_bounce` | system | | `soft_bounce_consecutive` | supported reason; not automatically created today | | `spam_complaint` | system | | `manual` | authorized teammate | | `legal` | authorized teammate | Suppressions change what sends: * Marketing email is skipped for any applicable channel-wide suppression. A topic opt-out also blocks the address for that topic within the workspace. * Transactional email is skipped only for a bounce suppression. Hard bounces and spam complaints create suppressions automatically as their events arrive. The consecutive-soft-bounce reason is supported by sending checks but is not automatically created by the current event handling. Authorized teammates can set `manual` and `legal` reasons. See [contacts](/contacts) for the full suppression model. ## Sender reputation A verified domain is what makes sending possible. Verification does not guarantee inbox placement. maxclicks shows you the signals it gets from the sending network: complaint records, bounce and complaint suppressions, and open recommendations for DKIM, DMARC, SPF, and BIMI. The workspace can have sending disabled separately from domain verification. Inspect its current reason and notices when a verified sender cannot send. Reputation is earned over time. Send to people who asked for your email, review bounce and complaint signals, and treat suppression as one sending control rather than proof that the remaining audience is appropriate. ## Read acceptance separately from delivery An individual email’s **Sent** status records the sending provider's acceptance. Campaign and template funnel totals can include pending or unsuccessful email records; use [report definitions](/guides/reports#read-the-rate-and-its-denominator) when assessing those totals. **Delivered** records a later delivery event; the message may still be placed outside the inbox. Opens and clicks are recorded interactions, not a revenue or activation measurement. Use the recipient's email activity to investigate failures and [Google Postmaster Tools](/postmaster) for Gmail-specific signals. When delivery is uncertain, a second send can become a duplicate. Reconcile the original message before sending the same customer another copy. ## Next With a verified domain and clean suppressions, you are ready to send. Continue to [sending email](/101/sending), or open the [domain](/domain) reference for the full DNS walkthrough and sender profiles. --- # Going further: connecting your own systems Source: https://docs.maxclicks.ai/101/developers # Going further: connecting your own systems Your product supplies the customer facts and business events; your marketing team owns the audience definitions, email content and journeys in maxclicks. Developers connect the two through the Public API, using the operations available for each resource. Use this lesson to prepare the developer handoff: source identifiers, event definitions, template inputs, workspace access and the operations your product will call. ## What your code drives, and what you build in the app Your code can: * create and update contacts and objects * fire events, which start [workflows](/101/workflows) and [webhooks](/webhooks) * send a saved [template](/templates) to one recipient * trigger, pause and unpause a workflow, and read its runs * update, schedule and send a [broadcast](/broadcast), and read its results * create segments and count or list who matches them * read back your schemas, attributes, domains, senders and topics You build in the app: * schemas and their attributes * email content: templates, broadcast content, and the content inside a workflow's send email step * workflows and forms * sending domains, sender profiles and communication topics * API keys Agree on the data model and email inputs before implementing the integration. Your developer needs the schema slugs, stable field keys, event identifiers and template identifier, plus the expected behavior on retries. The reference for each resource defines the exact available operations. ## Create a key Open **Settings**, then **Developers**, then the **API keys** tab, and click **Create API key**. A name is optional and exists only to tell keys apart later. The key is shown to you exactly once. Copy it then, because maxclicks never shows it again. A key is scoped to its workspace and inherits its owner's membership permissions; there are no granular scopes. Keep it in server-side configuration, and revoke it if exposed. ## Hand over the reference The [Quickstart](/quickstart) uses a key created in the app to authenticate, write a record and send a saved template. The [API reference](/api) then has every operation with request examples, plus how errors, paging, retries and rate limits behave. ## Send email over SMTP instead If the system that needs to send email speaks SMTP more comfortably than HTTP, open **Settings**, then **Developers**, then the **SMTP** tab. It hands you the host, the port and the username to copy, and any of your API keys works as the password. The SMTP message body is a JSON payload naming the saved [template](/guides/templates) and its input data. maxclicks renders the content authored in the app. That way an SMTP send is personalized and tracked exactly like every other send. ## Get told when something happens Webhooks run the other way: maxclicks calls a URL of yours. Create them under **Settings**, then **Developers**, then the **Webhooks** tab. You can be called when a contact is upserted or deleted, an object is upserted or deleted, an event is fired, or an email event happens such as a bounce or a click. If your endpoint keeps failing, maxclicks stops calling it. The webhook shows **Circuit breaker active**, with how many calls failed in a row and the last error it saw, and nothing is delivered until you click **Reset and resume**. Check the Webhooks tab after you deploy changes to a receiving endpoint. The [Webhooks](/webhooks) reference covers the payloads and how to verify that a call really came from maxclicks. ## Hand your space to an AI agent The [MCP server](/mcp-server) is the tool interface for an external AI assistant to operate supported Public API actions. Follow its connection instructions to configure your assistant with your maxclicks API key. It goes through the same API with the same key, so it can do exactly what that key can do and nothing more. Note the direction: maxinja is the AI inside maxclicks that builds things for you, and the MCP server is the door your own AI reaches in through. ## Choose your development tools Use the [SDK catalog](/sdks) to choose a library for your application, the [CLI](/ai/cli) for terminal commands and scripts, or the [MCP server](/mcp-server) to connect an AI assistant. Each guide includes installation and authentication instructions. [Agent skills](/ai/skills) give coding assistants maxclicks task guidance. Clients and agent tools use the Public API with your key, its permissions, and its [rate limits](/api/rate-limit). The [HTTP quickstart](/quickstart) shows the requests directly. SMTP is a separate way to submit a prepared template send. ## Next That is every surface of maxclicks: your data, maxinja, sending, automation, and the programmatic doors. The last stop is a quick reference for the vocabulary you picked up along the way. [maxclicks glossary](/101/glossary) --- # Designing and personalizing email Source: https://docs.maxclicks.ai/101/email # Designing and personalizing email Your team owns the message across every sending path. The shared composer lets you build a branded email for a [broadcast](/101/sending), [template](/templates) or [workflow](/workflows) step, select the customer context, and inspect recipient previews. ## Four content modes Choose the editor around the message you want to build. | Choose | When it fits | | --------------------------------------- | ------------------------------------------------------------------------------------------- | | **[Dynamic Composer](/editor/dynamic)** | A visual newsletter or announcement with images, sections and columns that adapt to mobile. | | **[Plain Composer](/editor/plain)** | A direct note with text, a button and a simple top-to-bottom layout. | | **[MJML Code](/editor/mjml-code)** | Your team already has MJML or wants to author a responsive layout in code. | | **[HTML Code](/editor/html-code)** | Your team brings finished email HTML and will maintain its layout. | The choice is made when the email is created. All four support Liquid and Adaptyle. The visual editors can make certain permanent moves to code, and Dynamic Composer can move to Plain Composer; moving to Plain drops preview text and image links. Check the [switching rules](/editor#moving-to-code-later) before changing an existing email. The API cannot write email content. It reads a template's content back as raw markup and sends it; the subject and body are authored in the app or by maxinja. ## Try a simple announcement Open a draft broadcast in **Dynamic Composer**. Add a short subject, one paragraph announcing the product and a button to its page. Select a verified **From** sender and your published brand kit. In **Data**, inspect the contact key and choose a sample customer. Start with a greeting such as `Hi {{ contact.firstName | default: "there" }},`, replacing `contact` with the key the panel actually shows. Preview a named customer and an unnamed one: the greetings should become “Hi Ada,” and “Hi there,”. That is a useful first personalization before adding generated copy. ## Data and branding An email lists the information it needs, like the contact and their order. Each entry has a name, such as `contact` or `order`, and you use those names when you personalize. maxclicks adds one more for free on every send, `branding`, so the selected [brand kit](/brandkits) (colors, fonts, assets, voice) is there without you passing it in. See [schemas](/schemas) for how the underlying records are defined. ## Two layers of personalization Rendering first resolves supplied Liquid values. An AI pass runs when the email contains Adaptyle instructions. **Liquid** is how you drop in values you already have, plus a little if-this-then-that logic. It applies deterministic rules to the data for that render without a writing-model call. Changing data or time-based expressions can change the result. ```liquid Hi {{ contact.firstName | default: "there" }}, {% if order.total > 100 %}Thank you for your order.{% endif %} ``` **Adaptyle** writes the passages you mark using the recipient context selected for that email. Any [evaluated fields](/evaluated-attributes) you select are resolved first. Describe the writing job and how to handle missing information, then inspect several recipient samples. Later generations can vary. You write these instructions straight into the content. Give an instruction on its own for the email’s selected runtime model to write something new. To have it rewrite text you already have, put the instruction first, then `:::`, then the text. ```text <<>> <<>> ``` The rendering model runs when Adaptyle instructions remain after Liquid resolves for that recipient. No remaining instructions means no generation call, while sending and application work remain separate. Corrections and campaign reviews can add model usage. Use AI where customer context changes the message. Keep essential offer amounts, dates and links in fixed content or Liquid. See [billing](/billing) for how usage is priced. ## Know what a preview proves A preview shows one sample at one moment. It helps you catch missing values, unsuitable wording and broken layout. A test email checks that rendered sample in an inbox. Neither establishes the production audience, consent, sufficient credits or campaign quality release. The editor’s **Data**, **Personalize** and **Checks** tabs make those inputs and issues visible. Compare several samples and read the estimate before sending to an audience. Next: [send your first email](/101/sending), or read the [editor reference](/editor) for every block, mode, and Adaptyle detail. --- # Glossary Source: https://docs.maxclicks.ai/101/glossary # Glossary Use this glossary to connect maxclicks terminology to the decisions your team makes. Follow each reference for the controls, limits and examples behind the definition. ## Space and data model * **Space**: the workspace that owns your customer data, campaigns, members and billing. Check which space you are in before importing or sending. See [workspace](/workspace). * **Schema**: the definition of a kind of record and its fields, such as Customer or Order. Its type is contact, object or business event. See [schemas](/schemas). * **Stored attribute**: a fact saved on a record, such as a plan or renewal date. A configured missing-value policy applies when a record is created. See [attributes](/attributes). * **Evaluated attribute**: a value derived from workspace data. **On demand** evaluates its definition when requested; **Scheduled** publishes an Event-history calculation on a configured interval. Both appear as Evaluated in the attribute catalog. See [evaluated attributes](/evaluated-attributes). * **Published calculation**: a complete Scheduled result, such as a customer's purchase count in a rolling window. Opening the value does not refresh it. A missing subject in a valid publication returns null; an unavailable or expired required publication can block the consumer. See [scheduled values](/attributes#computed-attributes). * **Attribute expansion**: explicitly requesting evaluated values, and any related records, for the data an email or workflow uses. Ordinary public record reads do not include those expanded values. See [attribute expansion](/attribute-expansion). * **Setup change**: a proposed change to schema or attribute definitions that needs dependency and value review. Review checks the proposal; Apply publishes the reviewed change. Generating a definition does not itself change existing values. See [setup changes](/setup-changes). * **Record**: one entry following a schema. An internal record ID, your product's external identifier and a person's email are different identifiers with different matching rules. See [records](/101/data-model/records). ## The three record types * **Contact**: a person your team can email, with identity fields, marketing subscriptions and email statistics. See [contacts](/contacts). * **Object**: a business entity such as an order, subscription or product. It can supply context but is not itself an email recipient. See [objects](/objects). * **Business event**: an occurrence such as PurchaseCompleted. Its accepted payload is immutable. Acceptance and readiness for downstream workflows are separate states. See [events](/events). **Event collection** binds an Event to a producer source, event key and subject contract. Its collection settings and typed payload determine which observations are accepted. Collection is configured from the Event's Collection page. See [collected events](/events#collected-events). An event's **subject** identifies the contact or object the occurrence belongs to. A custom property containing an email address does not by itself establish that authoritative link. A **correlation ID** can connect occurrences from the same journey or transaction; it does not replace the subject's identity. ## Audiences and messages * **Segment**: a saved filter over one contact schema. It describes a rule; matching contacts can change with the data. See [segments](/segments). * **Template**: a reusable email with expected data and recipient configuration, commonly sent from your product through the API. Workflow email steps own their own email content. See [templates](/templates). * **Broadcast**: a one-time audience campaign with its own email and delivery schedule. Preparation fixes a campaign revision and recipient selection. See [broadcasts](/broadcast). * **Brand kit**: published design assets, palettes, typography, footer and voice guidance. Saved edits change the live kit; they do not rewrite sent emails or frozen campaign content. See [brand kits](/brandkits). * **Sample data**: the values used to review an email in the editor. Editing a sample does not change a customer record. See [sample data](/editor/sample-data). * **Execution snapshot**: the saved campaign version or calculation generations used for consistent execution. A newer live value does not automatically replace the snapshot of work already running. See [data freshness](/attribute-expansion#know-how-fresh-the-value-is). ## Automation and personalization * **Workflow**: a journey made of triggers, actions, conditions and waits. Publishing saves the version that new runs use; existing runs retain their version. See [workflows](/workflows). * **Run**: one execution through a workflow. A split or qualifying later event can create another run with its own history. See [workflow runs](/workflows/run). * **Liquid**: deterministic insertion, formatting, conditions and loops over supplied data. It does not require a writing-model call. See [Liquid](/liquid). * **Adaptyle**: authored instructions that generate selected email passages. Liquid resolves first; the model runs only when prompts remain for that recipient. Fixed content is checked against the reference. See [Adaptyle](/adaptyle). * **maxinja**: the in-app assistant your team briefs to investigate and build saved work. Draft authoring, background workflows and recipient personalization are separate actions. See [maxinja](/maxinja). * **Runtime AI settings**: the model and thinking depth an email or AI workflow step uses when it executes. They are separate from the assistant conversation's model. See [workflow AI](/workflows/ai). * **Evaluated attribute threshold**: a workflow trigger for a published Scheduled value crossing a typed condition. Publishing its baseline does not immediately trigger the whole already-matching population. See [workflow blocks](/workflows/blocks). ## Sending and compliance * **Domain**: a sender domain verified using the DNS records maxclicks supplies. Verification and the account's ability to send are separate checks. See [domain setup](/domain). * **Marketing subscription**: permission recorded for email marketing. A communication topic adds a more specific subscription requirement when the email selects that topic. See [email types](/types-emails). * **Suppression**: a sending restriction associated with an address. Marketing and transactional sends apply different suppression rules; transactional status does not bypass delivery-failure suppression. See [contacts](/contacts). * **Test send**: a real transactional message containing the editor's rendered sample. It uses sending allowance but does not validate the production audience or marketing recipient mapping. See [email preview and tests](/editor#preview-it-before-you-send). ## Read the state before taking another action | Term | What it establishes | | ------------------------------- | -------------------------------------------------------------------------------------------------------------- | | **Accepted event** | maxclicks has durably accepted the occurrence; check its readiness before assuming downstream availability. | | **Ready event** | The event has reached the readiness state used by downstream consumers. | | **Scheduled broadcast** | A delivery time is set. Preparation can begin before that time and close editing. | | **Ready to send** in the editor | The draft's current readiness checks pass. Campaign preparation and actual execution have further checks. | | **Paused workflow** | New execution and existing runs wait; completed effects remain. | | **Stop new runs** | Workflow admission is stopped; existing runs can continue. | | **Terminated run** | Further execution of that run and its children is stopped; completed actions remain. | | **Stopped broadcast** | Remaining work is cancelled. It cannot be resumed as the same send. | | **Sent** | The email provider accepted the message. | | **Delivered** | A later provider delivery event was received; this does not establish inbox placement or a human read. | | **Unknown outcome** | Acceptance of an external action could not be confirmed. Inspect and reconcile before creating another action. | ## Working with developers and billing * **Public API**: the documented endpoints for integrating your product with maxclicks. App functionality and available SDK methods can have different coverage. See [API reference](/api). * **API key**: a secret credential for a workspace and its permissions. An attribute's API key label refers to its field identifier, not this authentication secret. See [authentication](/api/authentication). * **Idempotency key**: a reference reused for retries of one supported API operation. A new key represents a different operation; source event deduplication uses its own event identifier. See [idempotency](/api/idempotency). * **Webhook**: a delivery from maxclicks to your endpoint, or an incoming trigger endpoint for a workflow. Outgoing deliveries can retry, so the receiver should deduplicate them. See [webhooks](/webhooks). * **Credits**: units charged for applicable application work and AI usage. Recipient rendering, corrections and campaign reviews can add usage beyond writing the initial draft. See [billing](/billing). * **Prepaid workspace wallet**: all teammates and background automation share purchased credit for model/tool usage and email overage. There is no per-seat charge, included AI allowance or signup/domain-verification gift. See [billing](/billing). ## Next Start with the [data model](/101/data-model), or use the [guides](/guides) for a specific customer task. [/101/data-model](/101/data-model) --- # Getting your data in Source: https://docs.maxclicks.ai/101/importing-data # Getting your data in Bring the customer facts your campaigns need into the model you have defined. Use a CSV for an existing list, the app for individual records, forms for signups, and the Public API for ongoing updates from your product. Choose what one row represents before importing. A customer CSV has one row per person; an order CSV has one row per order and belongs in an Order object schema. Keep the customer identifier on each order so an [evaluated relationship](/101/data-model/relationships) can find the right shopper's purchases. Importing both files does not itself define that relationship. ## Upload a spreadsheet Start here if your list is already in a spreadsheet, which is how most first lists arrive. In the top navigation, open **Records** and pick the contact type your list belongs to. On that list, click the import button beside **Add**: it carries the type's plural name, so for a type called Customer it reads **Import Customers**. That opens the CSV import dialog. Click **Download sample CSV** before you do anything else. There is no column matching screen: maxclicks reads your header row as field keys, exactly and case sensitively, so `email` is understood and `Email` is skipped without a warning. The sample's header row is already the exact set of columns your type accepts, so filling your data in underneath it is the safest route. Add any tags you want on everyone in this file, then click **Upload CSV and start import**. A file can be up to 50 MB and 500,000 rows. The import runs in the background; leaving the page does not stop it. Watch the run on the same page. It shows **Pending**, **Running**, **Succeeded**, **Partial**, **Failed** or **Canceled**. The importer processes validated rows in checkpointed batches, so a run can finish **Partial**: it then tells you how many rows made it, how many failed, and gives you a failure log to download that says why each one failed. Blank cells are omitted rather than clearing existing values. New records use the field's missing-value policy; matched records do not rerun creation-time defaults or AI autofill. Unknown headers can be omitted without a visible column warning, so inspect several imported profiles after the run. During onboarding, maxinja can inspect a CSV sample and propose a mapping into an existing schema. Review that plan before starting the mapped import. This is separate from the standard dialog's exact-key format, and sample validation does not prove every row is valid. [Import a contact list](/guides/import) covers format, consent defaults, progress and recovery. Three things worth knowing before you upload: * **Objects import the same way**, but their lists have no import button. Go to **Settings**, then **Schemas**, open the type and use its **Import** tab. That page is where every import lives and is tracked, contacts included. * **Events cannot be imported from a CSV.** Use the event interface with occurrence identity and timestamps; supported occurrence-time limits apply. * **A type kept in sync by a connected tool cannot be imported into.** Its Import tab says which tool owns it, and the upload is turned off, because that tool adds and updates those records itself. ## Add someone by hand For the one lead who came in by phone, or a couple of test records before a launch, skip the spreadsheet. On a contact list, **Add** followed by the type's name opens a form with that type's fields. On an object list the same button reads **New** followed by the type's name. [Add your contacts](/guides/contacts) shows the form and how to edit someone afterwards. ## Collect people from your website A [form](/forms) collects the information and communication preferences a visitor chooses to provide. Build one under **Forms** in the top navigation, put it on your site, and every submission becomes a contact. A form can ask for email consent at the moment someone submits it, and it can require double opt-in: the person is held to one side, maxclicks emails them a single-use confirmation link, and they become a contact only when they click it. Double opt-in records that the person used the confirmation link; it does not guarantee future engagement. [Create a signup form](/guides/forms) covers building and embedding one. ## Write records from your own systems If your product already knows these people, keep them in sync instead of exporting a spreadsheet every month. Your own code can create and update contacts and objects and fire events as they happen, and a [workflow](/101/workflows) step can create records while it runs. [Connecting your own systems](/101/developers) covers what to hand a developer. ## Keep identity consistent Matching reduces duplicates only when you supply a consistent identity and choose the intended operation. For contacts without a platform ID, `userId` takes priority, otherwise email, otherwise phone. It does not try all three against unrelated profiles until one matches. Objects use platform ID or `externalId`. Repeated upserts of the same stable identity can update one record; input with no identity can create another. A supplied platform ID that does not exist can be reported as unmatched rather than created. Inspect failures and API responses, particularly when moving from email-only contacts to your application's user IDs. ## Only email the people who agreed Imported contacts are not subscribed to email unless you tick **Subscribe to email by default** in the import dialog. Tick it only for people who have explicitly agreed to hear from you. If your file has a `subscriptions` column, the value on the row wins over the checkbox. The checkbox reaches only the contacts this import creates: a row that matches someone already in your space leaves subscriptions unchanged unless the row explicitly supplies them. Use consented audiences and maintain the preferences people give you. maxclicks does not permit unsolicited cold email; importing an address is not evidence of permission to market to it. [Deliverability](/101/deliverability) explains what that costs you. The rest of the rules live with the records themselves: marketing messages require the relevant channel consent and, when assigned, the topic preference. The [Contacts](/contacts) page has the full picture. ## Knowing where a contact came from Every contact carries a **Source** field showing how it arrived: `import`, `api`, `form`, `manual`, `workflow` or `integration`. maxclicks sets it and nobody can edit it, not you and not your code, which is what makes it worth trusting when you are working out why a list looks the way it does. It is an ordinary field, so a [segment](/101/segments) can filter on it, for example the people who signed up through a form in the last month. ## Verify the data contract before scaling Choose three illustrative rows: a new customer, an existing customer with an updated plan, and a customer whose optional field is blank. Inspect the saved identity, plan, consent and custom fields. The blank should leave an existing value unchanged; the new record uses its creation policy. Compare those results with your intended mapping before importing the full list. If a field needs a different type or meaning, prepare a [setup change](/setup-changes) rather than trying to repair the schema through repeated imports. Review and complete the definition change, then use a newly prepared file or mapped plan for the corrected input. ## Next Once you have checked imported values, identities and consent, group the records into audiences. [Segments](/101/segments) --- # Meet maxinja Source: https://docs.maxclicks.ai/101/maxinja # Meet maxinja maxinja is the agent you brief inside maxclicks. It can investigate customer data, build an audience and email for a campaign, or put together a customer journey. You open the saved result, review it and give the next instruction. Imagine a coffee brand introducing a new roast to previous buyers. The marketer supplies the product facts, price and link, then asks: > Create a draft broadcast for customers with at least two completed orders and none in the last 60 days. Introduce the new roast using their purchase history. Keep the price and link fixed. Use our published brand kit and leave it unscheduled. The result to review is a campaign in the workspace: its matching customers, sender, email and schedule. maxinja can include Adaptyle instructions so the email’s introduction relates to each recipient’s purchase history. ## Describe, review, refine Three habits make a brief useful: 1. **Describe a specific customer action.** Name the audience, the intended action and the facts that must stay fixed. Supply source data and product details that are missing. 2. **Review the saved result.** Check a customer who should match and one who should not. Preview the email for people with different histories, including missing data. 3. **Refine the same work.** Attach the saved campaign and say exactly what to change: “Shorten the introduction to two sentences; keep the offer and link fixed.” For an audience, maxinja gives you a readable explanation of the condition. For example, a request for new customers who have not ordered might become: > Contacts created within the last 30 days with no related Order records. If that is too broad, refine it: “Only count completed orders.” Inspect the matching records after the change. ## Where you meet maxinja **The chat pane** stays beside your work. Use it for campaigns, reusable email templates, workflows and data investigations. Attach the current page with **Current view**, or use `@` to identify a saved item. The investigation tools read workspace data; the campaign and workflow tools can save changes. **Inline Ask maxinja controls** focus on the feature you are configuring: a segment’s audience, an evaluated attribute or workflow logic. The email editor’s input can refine the selected subject or block. Initial onboarding also offers setup assistance while the workspace is incomplete. See [the maxinja guide](/guides/maxinja) for a campaign you can follow. ## Separate authoring from execution | When | What happens | | --------------------------------- | ---------------------------------------------------------------------------------------------------------- | | **You brief maxinja.** | It creates or edits saved campaign content and settings. | | **Customer content is rendered.** | Adaptyle uses its saved instructions and supplied data to generate the marked passages for that recipient. | | **A published journey runs.** | Its saved workflow steps execute for that customer. AI steps call their configured model when reached. | A draft is saved work you can review. Include “leave it unscheduled” or “keep it unpublished” when that is the intended result. maxinja can send or publish when asked, without another assistant approval stage; product validation and permissions still apply. While it works, a new message steers the active request. Stopping keeps completed edits and actions. Reopen the saved item before continuing after an interruption. Workspace assistant runs require write access. For reusable activity measures, define a **Scheduled** evaluated attribute on the owning contact or object schema. maxinja can inspect its publication status and values, or request a refresh of its saved definition. Learn that separate setup in [scheduled customer signals](/maxinja#define-behavior-signals-with-tracks-and-traits) when your campaigns need it. ## What it costs maxinja access is included without a per-seat charge, subject to workspace write permission. All AI usage is paid separately from the shared workspace wallet. There is no monthly AI allowance. A campaign brief, revision and Adaptyle preview can each add usage; the model, context and work performed affect the total. Review [AI and usage credits](/billing/credits) and representative usage before scaling. ## Next [/101/navigating](/101/navigating) --- # Getting around your space Source: https://docs.maxclicks.ai/101/navigating # Getting around your space The workspace connects customer context, audiences, messages, journeys and results. This lesson shows where each part lives and how to move between businesses or brands without mixing their records. ## What a space is Everything you do in maxclicks happens inside a **space**, your business's home in the app. A space holds your contacts, schemas, segments, templates, broadcasts, workflows, forms, domains, senders, members, and billing. Data is isolated: nothing crosses from one space to another. Your account can belong to more than one space, one per business or brand. You move between them from the space menu (see [Switching spaces](#switching-spaces-and-settings) below). ### One space, fully isolated When you switch spaces, the entire app re-points at that space's data, members, and billing. See [Your workspace](/workspace) for the full model. ## The app shell Every page in your space sits under a URL like `/space/your-slug/...`. The app has a **top bar** for navigation and two panes below it: * The **maxinja assistant pane** on the left, resizable, which can take over the full view. This is where you talk to [maxinja](/maxinja). * The **main content** on the right, where you view and act on your data. The top bar holds the navigation, the notification center, and the space and account menu. You reach every area from the top bar; clicking an item swaps the main content. ## The main areas | Area | What it is for | | -------------- | ----------------------------------------------------------------------------------------------------------------------- | | **Records** | One table per contact or object [schema](/schemas). Search, create, import, and manage your [contacts](/contacts) here. | | **Segments** | Filter your audience into named groups. See [segments](/segments). | | **Brand Kits** | Your brand's colors, fonts, and assets, used when personalizing email. See [brand kits](/brandkits). | | **Workflows** | Automate a process as a graph of triggers, conditions, and actions. See [workflows](/workflows). | | **Templates** | Reusable email, sent one recipient at a time, usually from your own code. See [templates](/templates). | | **Broadcasts** | One email to a whole audience, now or on a schedule. See [broadcasts](/broadcast). | | **Forms** | Build and publish [forms](/forms) to collect contacts. | | **Metrics** | Space-level email performance. | | **Settings** | Everything you set up once: domains, senders, schemas, members, billing (below). | You do not need to learn all of these now. The point is that the top bar is the index: whatever you want is one click away. ## Switching spaces and settings The space and account menu in the top bar is where you leave the current space and manage it: * **Switch spaces**: jump between the spaces your account belongs to. * **Settings**: General, Brand, [Members](/members), Domains, Sender Profiles, maxinja, Schemas, Events, Topics, Plans, [Billing](/billing), Developers, and Forms Protection. What you can reach depends on your role. Only admins manage members, billing, and space-wide settings. ## Next Learn what fills the main content: how schemas, attributes, and records model your business. [/101/data-model](/101/data-model) --- # Audiences and segments Source: https://docs.maxclicks.ai/101/segments # Audiences and segments Before you send anything, you pick who receives it. A **segment** is a named filter over the contacts in one contact schema. You describe the audience you want in plain language, and maxinja builds the condition behind it. A segment evaluates the current customer data each time it is used. Its membership can change as those records change, so a saved audience is not a frozen list. The campaign still applies sending eligibility and consent checks. ## Segments filter contacts Each segment returns people from one [contact schema](/101/data-model). It can use objects and events to decide which people qualify. “Customers with at least two completed orders” returns customers; each customer's Order objects supply the evidence. For a shared account, ask whether your rule concerns the person or the whole account. Maya may have no projects of her own while Daniel has one. An account-wide project count would treat them alike; a count linked to each project's creator keeps their progress separate. ## Describe it, maxinja builds it You do not stack dropdowns or write a formula. You write a requirement in plain language and maxinja generates the condition: * Contacts who signed up in the last 30 days but have not placed an order. * Paying customers whose company has more than 100 employees. A condition can use [evaluated attributes](/101/data-model), which reach into related records. So a segment can filter on data you never stored on the contact, like "contacts whose company is in the Tech industry." A segment can also build on another segment, and maxclicks stops you deleting a segment that another one depends on. ## Read the description, not the code Each segment keeps three things: the requirement you wrote, a readable description of the condition maxinja built from it, and the logic itself. The logic stays on maxclicks servers and is never sent to your browser or the API. You confirm a segment by reading its description, not by reading code. Before you save, the preview shows a count and a searchable table of matching contacts. Check a known inclusion, a known exclusion and a missing-data case. Counts refresh with the first page of a new listing request and are reused while you page; the table is not a frozen audience snapshot. For a rule based on a scheduled evaluated attribute, inspect **Data through** on its published result. Empty history is not zero and refreshing the segment does not rerun the background calculation. Tell maxinja how the audience should treat missing values. ### AI is required here Defining a segment always goes through maxinja, and each generation consumes credits. There is no manual rule builder. ## Where segments plug in A saved segment is a reusable audience definition. Each consuming feature applies it at its own point in the lifecycle; a broadcast seals its selected audience for that send rather than continuously adding later matches. | Feature | The segment becomes | | ---------------------------------- | ----------------------------------------------------------------------------------------------------- | | [Broadcast](/broadcast) | Candidate audience; consent, destination deduplication, suppressions and delivery checks still apply. | | [Workflow](/101/workflows) trigger | A narrowing, so a run starts only for matching contacts. | | Workflow step | A filter on a find-records step. | | [Webhook](/webhooks) | A filter on contact and email-event triggers. | Anywhere you can reference a segment, you can also describe a one-off condition inline instead. Saving it as a segment is what makes the same audience reusable across features. ## Next With the audience settled, the next question is what you send them and how one email personalizes for every contact in the segment. For the full feature detail, see the [segments reference](/segments). [Designing and personalizing email](/101/email) --- # Sending email Source: https://docs.maxclicks.ai/101/sending # Sending email Choose a sending path that matches the customer moment. Broadcasts reach a defined audience once; templates let your product trigger a saved message for a recipient; workflows send as part of a journey. All use the same composer and the sending mode set on the email. | You want to… | Choose | Example | | ------------------------------------ | ------------------------------ | ---------------------------------------------------------------- | | Reach a group at a time you choose | [Broadcast](/guides/broadcast) | Announce a new coffee roast this Friday. | | Let your product request one message | [Template](/guides/templates) | Confirm the order that just completed. | | Respond with a sequence over time | [Workflow](/guides/workflows) | Wait after an order, check for another purchase, then follow up. | The sending path and the sending mode answer different questions. Choose the path for timing and orchestration; choose marketing or transactional for the message’s purpose and consent behavior. ## Templates: send per recipient via the API A template is saved email content plus a list of the information it needs. You send it from your application with an API call, one recipient at a time. Use it for receipts, password resets, order confirmations, and welcome emails. When you send, you pass only enough to identify each record, for example `{ "email": "ada@example.com" }`. maxclicks finds or creates that contact, reads the full record back, loads the attribute expansions configured for the email, builds the email, and sends it. Sending a template writes data. Each record you pass in is saved before the email is built, so a send that includes `firstName` also updates that contact's name. ## Broadcasts: mass-send to an audience A broadcast is a one-time campaign sent to a group at once. Use it for newsletters and announcements. Each broadcast targets one contact schema, then narrows to an audience one of three ways: * **All contacts** in that schema. * **A saved [segment](/101/segments).** * **A custom audience filter** you describe in plain language, which maxinja turns into a readable condition. Then you pick timing. A broadcast is one-time: send now, or schedule for a date and time. There is no recurring schedule. For anything that repeats in response to what people do, use a workflow. The [broadcast reference](/broadcast) covers composing, audience targeting, scheduling, and the send pipeline. ## Workflows: send within a customer journey A workflow starts from a configured trigger and can wait, look up data and choose what happens next. Its **Send Email** step carries its own email content and receives data from earlier steps. Write and test the draft, then publish the workflow before expecting new runs to use it. Start with the [workflows lesson](/101/workflows). ## Understand when a send becomes fixed A broadcast can prepare messages before its scheduled dispatch time. Preparation fixes the selected recipients and campaign revision; later audience changes do not add customers to that run. Before preparation you can **Cancel send** and edit again. After it begins, **Stop sending** permanently cancels remaining work, while messages already being sent may still arrive. A saved template is different: new requests use the current saved content, with no publication step. Published workflow runs keep the email templates in their version. Review those timing differences before editing a message used in production. An individual email’s **Sent** status means provider acceptance. Delivery, opens and clicks are later signals. The campaign funnel’s Sent total counts its email records; inspect their statuses before equating that total with accepted messages. See [report definitions](/guides/reports#read-the-rate-and-its-denominator). Check the email history before retrying or starting a replacement campaign, especially when acceptance is uncertain. ## Marketing vs transactional Whether an email is marketing or transactional belongs to the content, not to how you send it. You set it on the email itself, so a template, a broadcast, or a workflow can each send either kind. It decides who actually receives the email once consent and suppressions are applied. | | Marketing | Transactional | | ------------------------- | --------------------------- | ---------------------------- | | Requires consent? | Yes: subscription and topic | No | | Skipped on a suppression? | Yes, on any suppression | Only on a bounce suppression | | Typical use | Newsletters, promotions | Receipts, resets, alerts | A message to a bare email address, one that is not a contact yet, can only be transactional. Marketing requires a consenting [contact](/101/data-model/records) behind the address. ## Next Workflows are the third way to send, automatically and in response to events. Continue to the [workflows lesson](/101/workflows), or open the [broadcast reference](/broadcast) for the full send setup. --- # Automating with workflows Source: https://docs.maxclicks.ai/101/workflows # Automating with workflows A customer signs up, then creates their first project the next day. Sending “create your first project” two days after signup would miss what they have already done. A workflow can check their progress before deciding to send. In maxclicks, you describe that journey to maxinja or build it on a canvas, review the steps and publish. Each qualifying trigger starts a **run**: one execution whose decisions and results you can inspect. ## Follow one customer | Stage | What it means for the signup | | --------------- | --------------------------------------------------------------------- | | Signup event | Identify the person who just joined. | | Find Records | Bring that person’s contact into the journey. | | Delay | Wait two days. | | Query | Count projects this person created. A teammate’s work does not count. | | If/Else | End if they have a project; continue if they have none. | | AI, when needed | Prepare a first-project suggestion from their stated goal. | | Send Email | Send the suggestion to that contact, subject to sending eligibility. | Query reads facts. If/Else applies a rule. AI interprets supplied context. You can use them together without asking a model to make every decision. For personalized wording directly inside an email, [Adaptyle](/adaptyle) is another option. [Build this signup workflow](/guides/workflows) when you are ready to try it. ## Triggers, steps, and links Every workflow is three things. * **Triggers** start a run. They have no incoming links. A trigger is a scheduled time, a record created or updated, a record deleted, an event fired, an email event (open, click, bounce), a Scheduled evaluated attribute crossing a threshold, or an incoming webhook. Every workflow needs at least one. * **Steps** do the work after the trigger. * **Links** connect steps. They set the order, and where a step branches, which way a run goes. Each step has ordered output points called **slots**, and each slot has at most one outgoing link. Loops are not allowed: a link cannot send a run back to an earlier step. The reference groups the 21 step types into five families (triggers, enhancers, controllers, actions, decorators), where controllers cover both branching and delays. See [Workflow blocks](/workflows/blocks) for the full catalog. ## A run carries data Each qualifying trigger starts a **run**: one execution of the workflow. An event can identify a customer, a webhook can start with a JSON payload, and a scheduled trigger can start empty. Later steps select the data they need from what earlier steps supplied and can add more. | After this step | The run's data holds | | --------------- | --------------------------------------------------- | | Event Fired | The signup event. | | Find Records | The event and the identified user. | | Query | The earlier context and the user’s project count. | | AI | The earlier context and a first-project suggestion. | | Send Email | Uses the configured recipient and message data. | The output of a step gets a name that later steps can reference. For example, Query can pass on `projectCheck`, and the branch can read its `hasProject` value. A step cannot use the answer from a step that has not run yet. Ordinary record values are read as steps need them. [Evaluated attributes](/evaluated-attributes) calculate on demand. Background [Scheduled evaluated attributes](/attributes#computed-attributes) use generations pinned for the root run and its children, so a run does not silently switch to a newer publication halfway through. Trigger and decision evidence can also be saved as a snapshot. See [attribute expansion](/attribute-expansion). Runs can be executing, waiting on a saved delay, parked by a workflow pause, or waiting for credits and capacity. A terminal failure ends a run, while temporary waits and safe infrastructure retries can continue. Inspect the recorded reason before re-triggering. ## You describe step logic to maxinja Most steps have one field where you describe what you want in plain language, and maxinja builds the logic. It is the same pattern you met in [segments](/101/segments): you type the outcome, maxinja builds it and shows a readable **description** rather than code, and you refine it with a follow-up. What maxinja produces fits the step: | Step | You describe | maxinja builds | | --------------------------------------- | ------------------ | --------------------------- | | `If/Else`, `Switch`, a filtered trigger | a yes/no question | a check that routes the run | | `Find Records`, `Delete Records` | who to act on | a database query | | `Set Value`, `Query` | a value to compute | the value and its shape | | `Delay` | how long to wait | a duration or a target time | The available-data panel in the editor shows every value the run is carrying at that step, so you know what you can reference. ## Ask the right question after a wait Place a lookup after the wait when you need to know what changed during it. In the example, Query counts the user’s own projects after two days; the branch then decides from that answer. Query errors and a valid count of zero are different outcomes. **If/Else** can also use **Customer event history** to check whether a chosen event has or has not occurred within a saved lookback window. Allow time for late events and choose whether the match is about the customer or one correlated transaction. Use **Evaluated attribute threshold** when a Scheduled value crosses a boundary. Publishing sets its baseline; already-matching customers do not all start a run. Use a separately reviewed audience if you need that initial campaign. ## Publishing, versions, and runs You build a workflow freely on the canvas, and steps can stay half-finished while you work. **Publishing** checks the whole graph (at least one trigger, no loops, no unfinished steps) and freezes it as a **version** that cannot change afterwards. * After any edit, the workflow is marked dirty until you publish again. * Only the latest published version starts new runs. Runs already in flight finish on the version they began on. * **Pause** prevents new runs and parks existing execution before more steps run. Saved delays keep their deadline. Use **Terminate run** to end a selected run, its children and pending continuations. Completed effects remain, and there is no rewind. * **Stop new runs** prevents new enrollment while existing runs continue. **Resume new runs** preserves a separate workflow pause. * Every step is checked again right before it runs, so a broken step is caught then and there. Every run records a full step history. Open the [run history](/workflows/run) to see which path a run took, the email it sent, and where anything failed. It is your first stop when an automation misbehaves. ## Next Build the [signup follow-up](/guides/workflows), or use [Workflow blocks](/workflows/blocks) to choose a step for another journey. [Deliverability basics](/101/deliverability) explains domain, consent and sending-health controls before you send. --- # Adaptyle Source: https://docs.maxclicks.ai/adaptyle # Adaptyle Adaptyle rewrites the passages you mark using each customer’s data. Write one email, then adapt its opening or product explanation for different people. Your layout, offer and links stay fixed. Use it in [templates](/templates), [broadcasts](/broadcast) and [workflow](/workflows) email steps. [maxinja](/maxinja) can build the email and its instructions from your brief, or you can add them in the [composer](/editor). ## One instruction, three customers Loopwell wants new signups to create their first project. The instruction asks for a project based on each person’s goal, progress and team context, without repeating work a teammate has already started. Maya’s email recommends launch sign-off, Leila’s recommends grouping interview notes, and Hugo’s recommends preparing a client handoff. These are saved, unedited Adaptyle outputs for fictional customers. The greeting uses a supplied first name; the generated paragraph changes; the **Create your first project** button stays fixed. ## Keep the facts. Adapt the message. | Content | Use | Loopwell example | | ------------------------- | ------------------------------------------ | ------------------------------------------------------------- | | A supplied value | [Liquid](/liquid) | The customer’s first name. | | A reason to act | Adaptyle | Suggest a first project from their goal and unfinished work. | | Required campaign content | Authored copy or Liquid outside AI regions | The **Create your first project** button and its destination. | Use Liquid when the answer is an existing value. Use Adaptyle when the message needs to explain what that value means for the customer. Generated wording can vary; a unique sentence for every recipient is not guaranteed. ## Before you begin * Open a draft email with a subject, sender and the content you want to personalize. * In **Data**, choose a sample contact and the fields the instruction needs. Select any [Evaluated attributes](/evaluated-attributes) and related records explicitly. * Select a published [brand kit](/brandkits) for its writing guidance. * Under **Personalize → Runs with**, set **Model** and **Thinking**. Previews and regenerations use AI credits. ## Write your first instruction 1. Select the paragraph in the visual composer and choose **Adaptyle** from the text menu. 2. Describe the writing job in the block’s instruction. For Loopwell: “Suggest a useful first project from their goal, progress and team context. Give one next step. Avoid work their teammates have already started.” 3. Keep the greeting, required campaign facts and button outside that block. 4. Generate a **Personalized** preview. Select another contact in **Data** and regenerate to compare the messages. 5. Check each message against its source facts, including a customer with missing context. Narrow or refine the instruction before sending. The instruction belongs to the template. The recipient sees the resulting email, without the Adaptyle block or its instruction. Follow the [personalization guide](/guides/personalize) for the complete editor walkthrough. ## Choose the customer data The rendering context includes the configured recipient data and selected published [brand kit](/brandkits). Stored customer fields and tags provide facts for the instruction. A campaign cannot use information that has not been supplied or selected. [On-demand evaluated fields](/evaluated-attributes), [scheduled evaluated fields](/attributes#computed-attributes) and related records must be selected explicitly. Open the **Data** tab, expand the contact row and select the fields your instruction needs. Check a sample value before relying on it in the message. Describe what to write from that context and what to do when a field is absent: ```text Suggest a first project from the customer’s supplied goal and progress. If the goal is absent, explain how to name a project and add one next step without assuming what the customer is working on. ``` ## Protect the parts that must stay fixed Text outside prompt regions is checked against the Liquid-resolved reference, with document-edge whitespace normalized. Hidden placeholders are checked for their position and occurrence count. The final email checks protect links, attributes, stylesheets and structural context from the AI rewrite. Keep buttons, image URLs, offer terms and required wording outside AI regions. Give prompts a writing task inside an existing layout. Asking a runtime prompt to add new styled sections, change link destinations or redesign the HTML can fail validation. Make those structural changes while authoring the draft instead. These checks establish content and structural boundaries. They do not prove that every generated statement is true, that an instruction achieved the desired tone, or that every email client displays the result identically. Compare representative recipient samples and send inbox tests. ## What it costs AI usage is metered for personalized rendering, including previews and regenerations. At campaign scale, each recipient whose content is personalized adds model usage. Content outside AI regions is preserved through the rewrite; Liquid still resolves its supplied values. Select the passages where context changes the message, such as the opening or product explanation. If no instructions remain after Liquid resolves for a recipient, that render skips the model call. Other recipients can still need AI. Sending allowances, application work, possible correction calls and campaign reviews are separate; see the [editor estimate](/editor#read-the-estimate-before-scaling-up) and [billing](/billing). An email with instructions requires its own model and thinking settings under **Personalize → Runs with**. Those settings are used during rendering; the workspace default is not a fallback. ## Resolve a failed personalization | Symptom | What it means | Next step | | -------------------------------------------- | ------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | | Model or thinking setting missing | The template’s runtime AI configuration is incomplete. | Set both in **Personalize**, then generate again. | | Adaptyle syntax error | A delimiter, nesting structure or manifest cannot be parsed. | Fix the marked source; retrying unchanged syntax will not help. | | Protected text, placeholder or link changed | The generated result exceeded its allowed edit scope. | Narrow the prompt and keep layout and essential facts outside it. | | Locale mismatch | Subject/body languages disagree, or the campaign’s approved locale no longer matches. | Align the manifest language and campaign settings. | | Empty subject/body or invalid final artifact | The result is not a sendable email. | Inspect the Base sample first, then the generated passage and markup. | | Credits or provider capacity unavailable | Rendering could not proceed with its current resources. | Check billing and the campaign/run status before restarting work. | Correction attempts may use additional model usage. Qualified broadcasts use the correction budget in their campaign policy; a failed render is not permission to send unvalidated output. See [broadcast preparation](/broadcast) for how this affects an audience send. If a broadcast is held for quality, open its **Quality review** tab and inspect the exact generated email, the source facts and the reported issue. An editor preview does not approve that output. Some noncritical issues allow a human decision with a reason; critical grounding, safety, privacy and consent issues cannot be overridden. Follow [review a quality hold](/broadcast#review-a-quality-hold) for the available actions. ## Why a later refresh may not change an active send A standalone sample resolves selected scheduled Evaluated fields against its current read context. A broadcast revision or root workflow run captures the calculation generation, freshness policy and observation time on its first read; child runs inherit that context. Publishing a newer calculation does not replace that captured selection. This fixes the scheduled calculation context, not every live customer field or on-demand lookup. An enabled calculation with no complete publication or an expired publication fails the expansion for that decision. Refreshing the calculation later does not repair an already captured unavailable selection. A paused definition can use its captured last-published policy; a valid publication with no subject result can return null. An erased pinned generation fails rather than switching to a newer one. Check the record’s **Published value**, **Data through** and status, then compare the sample with the execution context. See [attribute expansion](/attribute-expansion). ## Write instructions in a code editor Put an instruction between `<<<` and `>>>` anywhere in your subject or body: ```html

<<>>

``` The instruction is evaluated with each recipient's context. Generated wording can vary; distinct wording for every recipient is not guaranteed. ## Rewriting something you already wrote If you already have copy you like and want it tailored, put your instruction first, then `:::`, then your copy: ```html

<<>>

``` Everyone gets a version of your sentence rather than something written from scratch. Use this when you want to keep control of the message and only personalize the edges. ## Hide static markup from the model If a chunk of your email is styling or markup rather than words, wrap it in `<##` and `##>` with a name, a `:::`, then the content. The model sees only the name, and your content is put back exactly as you wrote it: ```html
> <<>>
``` Hidden blocks remove those bytes from the model’s view and restore them afterwards. This can reduce tokens for large static sections; a short placeholder can cost more than the text it replaces. Give each block a different name. Hidden content still becomes part of the final email and must pass its HTML checks. ## Add a writing brief and language In the MJML and HTML code editors you can add a short brief at the very top of the template, between `<<#` and `#>>`. It never appears in the email. Its only job is to give the model context. Use `description` for the writing brief. maxclicks combines the subject and body descriptions into one email context. Declare `locale` when the email needs an explicit language: ```yaml <<# locale: en-GB description: | Help new Loopwell customers create a first project. Audience: recent signups. Tone: clear and practical. #>> ``` The email renderer replaces your manifest `title` with its email title. Use `description` for instructions and `locale` for a valid language tag such as `en-GB` or `de-DE`. Subject and body locales must agree. A qualified AI broadcast requires an explicit locale matching its approved campaign settings. A manifest `tools` entry does not make an external tool available during email rendering. ## What happens during a render 1. maxclicks resolves the email’s declared data, selected expansions and branding. 2. Liquid resolves values, loops and conditions once. This establishes the reference content for that recipient. Values supplied by a contact cannot introduce new Adaptyle instructions. 3. Subject and body are processed together as one document. The model resolves the authored prompt regions with the whole email in view. Nested instructions are not a promise of one separate model call per prompt. 4. maxclicks checks that the output preserved protected content and hidden placeholders, resolved the instructions, and produced a valid subject and body. 5. The footer is added, MJML is compiled when needed, and the final HTML is validated before it becomes sendable content. The model receives the supplied data object and branding, not just the fields you mention in a sentence. Keep the declared data and expansions relevant to the email. It does not fetch an unspecified order history or generate a new image merely because an email prompt asks for one. ## Getting good results * **Describe the outcome, not the words.** "Write a one-line reason to come back, based on what they bought" beats "Say we miss them". * **Keep it to a handful.** More than 10 instructions in one email is a sign the email is doing too much, and maxclicks warns you. * **Nested prompts share one email context.** Subject and body are processed together; nesting does not create a guaranteed sequence of separate model calls. Keep the intended edit scope clear. More than two nesting levels triggers a warning. * **Inspect representative samples.** Compare the source fields with the generated message for an engaged customer, an infrequent customer and someone with missing context. A later render can produce different wording. * **Keep essential facts fixed.** Put amounts, deadlines, required wording and links outside AI regions. Inspect generated claims as well: preserving fixed blocks does not establish that every generated statement is accurate. ## Writing the symbols literally If you need `<<<` or `:::` to appear in the email as text rather than as an instruction, write the escape code instead and maxclicks prints the symbol: | To print | Write | | -------- | ------------- | | `<<<` | `<##ESCPO##>` | | `>>>` | `<##ESCPC##>` | | `:::` | `<##ESCS##>` | | `<##` | `<##ESCHO##>` | | `##>` | `<##ESCHC##>` | | `<<#` | `<##ESCMO##>` | | `#>>` | `<##ESCMC##>` | --- # Build with AI Source: https://docs.maxclicks.ai/ai # Build with AI Connect your coding agent to customer data, events, and prepared campaigns through the maxclicks Public API. Start with the [HTTP quickstart](/quickstart): verify your workspace, sync a contact, then send a template your team has prepared. No maxclicks package is required. Store an [API key](/api/authentication) in your server or agent environment as `MAXCLICKS_API_KEY`, then make a read request: ```bash curl --fail-with-body https://api.maxclicks.ai/v1/me \ -H "Authorization: Bearer $MAXCLICKS_API_KEY" ``` Check that `data.space` is the workspace you intend to use. Give your agent the [current API reference](/api) and ask it to inspect schemas and attributes before writing records. Schemas, content, sending settings, and journeys are authored in the app; the public API supplies data, triggers prepared work, and reads results. | Your next task | Start here | | ---------------------------------------------------- | ------------------------------------- | | Connect customer data and send a prepared email | [API quickstart](/quickstart) | | Give a coding agent the right context and first task | [Onboard an AI agent](/ai/onboarding) | | Find request shapes and runnable examples | [API reference](/api) | | Use maxinja to build a campaign inside maxclicks | [maxinja](/maxinja) | ## MCP server The MCP server exposes API operations as tools for compatible assistants. Connect to `https://mcp.maxclicks.ai/mcp` and approve the connection in maxclicks, or run the `maxclicks-mcp` npm package locally with stdio. The [MCP reference](/ai/mcp) documents configuration, authentication, and the available tools. ## CLI The CLI provides JSON commands for shell scripts and agents. Install it with `npm install -g maxclicks-cli`, then run `maxclicks login` and `maxclicks whoami`. The [CLI reference](/ai/cli) covers configuration, resource commands, and JSON output. ## Agent skills Skills teach your coding agent the maxclicks data model, API conventions, and common integration tasks. Install them with `npx skills add https://maxclicks.ai`, then configure the CLI, SDK, or MCP connection your agent will use. [Agent skills](/ai/skills) describes the catalog and setup. ## SDKs The [client library reference](/sdks) covers eight SDKs with installation instructions, authentication, and examples. Choose the SDK for your application’s language, or use the direct HTTP examples in the [API reference](/api). --- # CLI Source: https://docs.maxclicks.ai/ai/cli # CLI Use maxclicks from a terminal, scheduled script, or coding agent. The CLI wraps the [Public API](/api) and returns JSON, so customer updates and campaign results can fit into your existing automation. It wraps the same API as the [SDKs](/sdks). Use the CLI for shell and agent work, and an SDK inside application code. Like the API, it triggers, reads, and measures what you built in the app. It does not build those things for you. ## Current API coverage The CLI uses the [Public API](/api) for customer records, events, prepared campaigns, workflows, and webhooks. Follow each operation’s response and status fields: an accepted request can still require event readiness checks, deletion polling, or delivery tracking. ## Install Requires Node.js 18 or later. Install the package globally to make the `maxclicks` command available: ```bash npm install -g maxclicks-cli maxclicks login maxclicks whoami ``` For a single command without a global install, use `npx maxclicks-cli whoami`. Authenticate with `maxclicks login` or set `MAXCLICKS_API_KEY` as described below. ## Authenticate `maxclicks login` creates an API key for this machine by pairing through your browser. ### Run login `maxclicks login` opens your browser to `https://app.maxclicks.ai/cli/authorize`. The CLI starts a loopback listener and also polls for the result. ### Approve in the browser Sign in and approve the pairing. maxclicks creates a new API key and hands it back to the CLI over that loopback connection. The browser never shows you the key. ### Key is stored The key is written to `~/.config/maxclicks/config.json` for the active profile. Login times out after 5 minutes. For SSH or headless hosts, pass `--no-browser`: the CLI prints the authorize URL and polls until you approve it elsewhere. You can also set a key directly instead of the browser flow: ```bash maxclicks config set-key max_... # writes ~/.config/maxclicks/config.json # or per invocation: export MAXCLICKS_API_KEY=max_... maxclicks whoami # confirm the key, its owner, and its space ``` Keys are created and revoked in the app, under a space's Settings, in Developers. See [Authentication](/api/authentication). ## Configuration The CLI looks for your key in three places, in this order: the command flag, then the environment variable, then the config file. If one is missing, it tries the next. `--api-key` (flag) API key for this one command. Overrides `MAXCLICKS_API_KEY` and the config file. `--base-url` (flag) API base URL. Defaults to `https://api.maxclicks.ai/v1`. Use a staging URL here or via `MAXCLICKS_BASE_URL`. `--profile` (flag) Config profile name. Defaults to `default`. Keep one profile per space. `--web-origin` (flag) Web app origin used by `login`. Defaults to `https://app.maxclicks.ai`. Environment variables: `MAXCLICKS_API_KEY`, `MAXCLICKS_BASE_URL`, `MAXCLICKS_WEB_ORIGIN`, `MAXCLICKS_PROFILE`, `XDG_CONFIG_HOME`. The config file holds one entry per profile and is written with permissions `0600`. Look at it with `maxclicks config show` (the key is masked) or find it with `maxclicks config path`. `maxclicks logout` clears the stored key for a profile. The key still works until you delete it in the app, so delete it there too if you want it revoked. ## Commands Commands are grouped by resource: `maxclicks [args]`. Run `maxclicks --help` for details on any command. | Group | Actions | | -------------- | ---------------------------------------------------------------------------------------------------------- | | `whoami` | Show the calling key, its owner, and the bound space. | | `schemas` | `list`, `get` | | `attributes` | `list` | | `records` | `list`, `get`, `create`, `upsert`, `update`, `delete`, `deletion-status`, `cancel-deletion`, `audit-trail` | | `events` | `fire`, `fire-batch`, `status`, `identify`, `list` | | `templates` | `list`, `get`, `send` | | `broadcasts` | `list`, `get`, `update`, `send`, `runs`, `metrics` | | `segments` | `list`, `get`, `count`, `contacts`, `create`, `delete` | | `workflows` | `list`, `get`, `runs`, `get-run`, `pause`, `unpause`, `trigger` | | `webhooks` | `list`, `get`, `create`, `update`, `delete`, `rotate-secret` | | `suppressions` | `list`, `create`, `delete`, `batch-add`, `batch-remove` | | `domains` | `list`, `get` | | `senders` | `list` | | `topics` | `list`, `get` | | `api` | Raw authenticated request to any path. | | `docs` | Print documentation and machine-readable reference links. | Every record and event belongs to a [schema](/schemas), so pass the schema id or slug as the first argument (`events list` is the one exception, covered under Pagination below). Schemas and [attributes](/attributes) are created and configured in the app; the public API exposes their read operations. Inspect those definitions before writing records, and use the [API reference](/api) for request fields and response details. ```bash maxclicks schemas list --type contact maxclicks attributes list students maxclicks records upsert students --data '{"email":"ada@example.com","firstName":"Ada"}' maxclicks records get students cm7d3k9x40001qw8h2p5tvz9r maxclicks events fire order-placed --data '{"eventId":"ord_1","amount":42}' maxclicks templates send cm7d3kb7t0002qw8h9m4e0xls --data '{"contact":{"email":"ada@example.com"}}' maxclicks workflows trigger 7Qk2ZbN4x9LmR3vTpW8sHc0F --data '{"anything":true}' ``` Prefer `upsert` over `create`. Upsert matches on the record's [identity](/contacts), so running it twice updates one record rather than failing. `create` returns `409 identifier_conflict` when that identity already exists. ### Pagination List commands accept `--limit` (1 to 200) and `--offset`. Pass `--all` to fetch every page and return one flat JSON array. `schemas list` also takes `--type contact|object|event`. ```bash maxclicks records list students --all | jq '.[] | .email' ``` `events list` takes its event schema as `--schema ` rather than a positional argument, and is cursor-paginated (newest first): use `--cursor`, `--from`, `--to`, and `--limit`, or `--all`. ### Idempotency Use `--idempotency-key` with `records create`, `records upsert`, `records delete`, `templates send`, `broadcasts send`, `workflows trigger`, `suppressions batch-add`, and `suppressions batch-remove` to retain the same logical write across retries. `templates send` and `records delete` always send a key: when you pass none they generate one and print it as `idempotencyKey`, so retry a timed-out call with that value rather than running it fresh. `records delete` prints the deletion operation; follow it with `records deletion-status `. Reuse the key with the same request to replay its recorded result. For direct HTTP requests, send the `Idempotency-Key` header on supported operations. Recorded failures can replay, and unresolved operations require reconciliation. See [Idempotency](/api/idempotency). ### Raw requests For anything the commands above do not cover, `maxclicks api` sends an authenticated request to any path under the base URL. It returns `{ status, body }` and sends only the `Authorization`, `Accept`, and `Content-Type` headers, so it cannot carry an idempotency key. ```bash maxclicks api GET /me maxclicks api GET '/segments?limit=5' maxclicks api POST /webhooks --data '{"url":"https://example.com/hook","trigger":"contact upserted","schemaId":"students"}' ``` ## Output Output is always JSON on stdout. There is no human-readable mode. A single object comes back as-is. A list command returns `{ data, pagination, warnings }`, or a flat array with `--all`. Errors print to stderr and exit non-zero, so `set -e` and agent tool runners catch them. ```bash maxclicks whoami | jq '.space.slug' ``` ## Use inside agents Each command produces JSON and a non-zero exit code on failure, so agents can inspect results and stop when a request fails. * Authenticate once with `maxclicks login`, or inject `MAXCLICKS_API_KEY` into the agent's environment. * Learn the space first: run `maxclicks schemas list`, then `maxclicks attributes list `. Records are flat, so put base fields and your own attribute keys at the top level of `--data`. * Parse stdout as JSON. On a non-zero exit, read stderr for the message. * `maxclicks docs [query]` returns links to the documentation, the OpenAPI spec, and `llms.txt` for grounding. Install the [Agent Skills](/ai/skills) to give your agent the CLI conventions, data model, and integration guidance. ## Related * [SDKs](/sdks) * [API reference](/api) * [Agent Skills](/ai/skills) --- # Use maxclicks in Bolt Source: https://docs.maxclicks.ai/ai/guides/bolt # Use maxclicks in Bolt Connect the app you build in Bolt to customer records, events, and prepared emails in maxclicks. Keep the API key in a server environment and make the first request from backend code. Create a key at `https://app.maxclicks.ai/space/-/settings/developers`. A key is scoped to one space. Some operations require an admin key. Start with the HTTP request below to verify your workspace, then install the Node SDK for typed API calls in your application. ## Make a first request over HTTP This request runs in server-side JavaScript and needs no maxclicks package. Set `MAXCLICKS_API_KEY` in your server's secret store, then confirm the returned space before making a write. ```javascript const apiKey = process.env.MAXCLICKS_API_KEY if (!apiKey) throw new Error('Set MAXCLICKS_API_KEY first') const response = await fetch('https://api.maxclicks.ai/v1/me', { headers: { Authorization: `Bearer ${apiKey}` }, }) const result = await response.json() if (!response.ok) throw new Error(result.error?.message ?? 'Request failed') console.log(result.data.space) ``` For a runtime without `process.env`, use its server-side secret API. Keep the key out of browser bundles. Follow the [API quickstart](/quickstart) to sync a contact and send a prepared template. The examples below use the [Node SDK](/sdks/node), which provides types, pagination helpers, and typed errors. ## Connect the SDK ### Install the package Install the SDK in your application: ```bash npm install maxclicks ``` Requires Node.js 18 or newer. Ships both ESM and CommonJS builds. ### Store the API key Add `MAXCLICKS_API_KEY` to Bolt's environment variables. Never hardcode the key in client code. Call maxclicks from a server route so the key stays server-side. ### Call the API ```ts import { Maxclicks } from 'maxclicks' const mc = new Maxclicks() // reads MAXCLICKS_API_KEY // Confirm the key and its space const me = await mc.me() console.log(me.space?.name) // Upsert a contact by identity (id or slug of the contact schema) const contact = await mc.records.upsert('students', { email: 'ada@example.com', firstName: 'Ada', tags: ['signup'], }) // Fire an event, which can trigger a workflow or a webhook await mc.events.fire('purchase-completed', { eventId: 'ord_123', amount: 42 }) // Send a template, resolving the recipient by email await mc.templates.send(templateId, { data: { contact: { email: 'ada@example.com' } }, }) ``` Keep the API key on the server. A key in browser code is exposed to every visitor. Route SDK calls through a Bolt backend route, not the client bundle. ## Configuration Pass options to the constructor, or set `MAXCLICKS_API_KEY` and call `new Maxclicks()`. `apiKey` (string) Space API key. Defaults to `MAXCLICKS_API_KEY`. `baseUrl` (string) API base. Defaults to `https://api.maxclicks.ai/v1`. `timeoutMs` (number) Per-request timeout. Defaults to `60000`. `maxRetries` (number) Extra attempts on `429`, `5xx`, and transport errors. Defaults to `2`. `onWarning` (function) Callback for non-fatal API warnings. ## Errors On success a call returns the data directly. On failure it throws a typed error, one of the `MaxclicksError` subclasses. ```ts import { MaxclicksNotFoundError, MaxclicksRateLimitError } from 'maxclicks' try { await mc.records.get('students', 'missing-id') } catch (error) { if (error instanceof MaxclicksNotFoundError) { // 404 } else if (error instanceof MaxclicksRateLimitError) { console.log('retry after', error.retryAfterMs) } } ``` | Error | When | | ------------------------------ | ------------------------------------------------- | | `MaxclicksBadRequestError` | 400 validation failure | | `MaxclicksAuthenticationError` | 401 missing or invalid API key | | `MaxclicksPermissionError` | 403 insufficient permission or space out of scope | | `MaxclicksNotFoundError` | 404 resource not found | | `MaxclicksConflictError` | 409 identifier or uniqueness conflict | | `MaxclicksRateLimitError` | 429 rate limited (carries `retryAfterMs`) | | `MaxclicksServerError` | 5xx server error | The SDK retries `429` within its budget. It retries `5xx` and transport errors on reads and keyed writes, with backoff and `Retry-After`. Preserve the operation key and payload; reconcile uncertain outcomes. ## Give the Bolt agent write access Start with the [agent onboarding brief](/ai/onboarding): give the agent the current API reference, an environment-based key, and a read-only first task. It can write the HTTP integration without installing a maxclicks client. To give your agent callable tools, connect the [MCP server](/ai/mcp). Verify the connection with `whoami` before using tools that change records or send email. ## Related * [maxclicks Node SDK](/sdks/node) * [MCP server](/ai/mcp) * [Contacts](/contacts) --- # Use maxclicks in Cursor Source: https://docs.maxclicks.ai/ai/guides/cursor # Use maxclicks in Cursor Use the [agent onboarding brief](/ai/onboarding) to connect your workspace, then add MCP tools or agent skills using the setup below. For direct HTTP requests, follow the [HTTP quickstart](/quickstart). Give Cursor the workspace context it needs to build your maxclicks integration. Begin with reads before changing records or sending email. | Option | What the agent gets | Add it in | | --------------- | ----------------------------------------------------------------------- | ---------------------- | | MCP server | Tools that map to the Public API v1, callable directly. | `.cursor/mcp.json` | | maxclicks skill | 13 skills that teach the data model, the `maxclicks` CLI, and the SDKs. | Agent skills directory | ## Get an API key Create a key at [app.maxclicks.ai/space/-/settings/developers](https://app.maxclicks.ai/space/-/settings/developers). A key is scoped to one space. Some operations need an admin key (suppressions). Keys are created in the app, not over the API. Never commit the key. ## Add the MCP server Cursor reads MCP servers from `.cursor/mcp.json` in a project, or `~/.cursor/mcp.json` for every project. Add a `maxclicks` entry, then reload the window. ### Remote (hosted) Connect to `https://mcp.maxclicks.ai/mcp`. A client that supports MCP sign-in opens maxclicks so you can choose the workspace and approve what it may do; otherwise configure it to send your API key in its `Authorization: Bearer` header. Verify `whoami` before any write. ```json { "mcpServers": { "maxclicks": { "url": "https://mcp.maxclicks.ai/mcp" } } } ``` After you reload, Cursor asks you to sign in to the server and opens the maxclicks consent page in your browser. ### Local (stdio) Requires Node.js 18 or newer. Configure the client to run the npm package with your API key: ```json { "mcpServers": { "maxclicks": { "command": "npx", "args": ["-y", "maxclicks-mcp"], "env": { "MAXCLICKS_API_KEY": "YOUR_API_KEY" } } } } ``` Keep this configuration out of version control. Confirm the host can start the local process and successfully call `whoami`. ### Tools The tools cover records, events, segments, webhooks, suppressions, workflow triggers and runs, template sends, and broadcast sends and metrics. They can also read (but not change) your schemas, attributes, domains, senders, and topics. Create schemas, attributes, templates, broadcasts and sending settings in the maxclicks app, then use the supported tools to read definitions and act on prepared work. Broadcast operations can update supported fields such as the name, audience or schedule and request a send. The [API reference](/api) defines request fields, response states, and permissions. Review the saved campaign and its readiness before requesting execution. Call `list_schemas` and `list_attributes` to learn how your data is shaped, then write records with `upsert_record`. Confirm the connection with `whoami`: it returns the key, who owns it, and which space it belongs to. Every call belongs to a schema, and records are flat. Put base fields and your own attribute keys at the top level of a record, not nested under a `data` key. If you write a key the schema does not define, it is dropped and returned as a warning, not an error. ## Add the maxclicks skill The skill teaches Cursor's agent how to work with maxclicks: how to authenticate, how your schemas shape every call, when to reach for the CLI or an SDK, and which mistakes to avoid. It follows the open [Agent Skills](https://agentskills.io) standard, so Cursor loads it like any other skill. Install the skills in your project: ```bash npx skills add https://maxclicks.ai ``` See [Agent Skills](/ai/skills) for setup and the catalog. For terminal requests, install the [CLI](/ai/cli#install) and configure your API key. The skill catalog covers these areas: | Skill | Loads for | | -------------------------- | ----------------------------------------------------------------------------------- | | `maxclicks` | The entry point: auth, the data model, and the CLI/SDK/API map. | | `maxclicks-contacts` | Creating, updating, or deleting contacts and objects. | | `maxclicks-events` | Firing and listing events. | | `maxclicks-email` | Sending templates and managing suppressions. | | `maxclicks-templates` | How a template's data shape, recipient path, and sender fit together before a send. | | `maxclicks-adaptyle` | Writing a template's content in Adaptyle over Liquid. | | `maxclicks-segments` | Building AI-generated contact filters. | | `maxclicks-broadcasts` | Scheduling and sending broadcasts, and reading their runs and metrics. | | `maxclicks-deliverability` | Sending domains and saved senders, set up in the app and read over the API. | | `maxclicks-topics` | Communication topics and the consent they carry. | | `maxclicks-imports` | The app's bulk CSV import of contacts or objects. | | `maxclicks-workflows` | Triggering workflows and observing runs. | | `maxclicks-webhooks` | Managing outgoing webhooks and verifying signatures. | ## Which to use Use MCP for tools the agent can call, skills for task-specific guidance, and the CLI for terminal commands. For generated application code, use an [SDK](/sdks) or direct HTTP. These options share the same [Public API](/api). ## Build Give Cursor the [agent onboarding brief](/ai/onboarding). Set `MAXCLICKS_API_KEY` in its environment, then verify the workspace with a read: ```bash curl --fail-with-body https://api.maxclicks.ai/v1/me \ -H "Authorization: Bearer $MAXCLICKS_API_KEY" ``` Continue with the [API quickstart](/quickstart) to inspect a contact schema and upsert a test record. ## Related * [MCP server](/ai/mcp) * [Agent skills](/ai/skills) * [CLI](/ai/cli) --- # Use maxclicks in Lovable Source: https://docs.maxclicks.ai/ai/guides/lovable # Use maxclicks in Lovable Connect the app you build in Lovable to customer records, events, and prepared emails in maxclicks. Use backend code for API calls, or an MCP connection for an assistant that needs to work with your workspace. Create a key at `https://app.maxclicks.ai/space/-/settings/developers`. A key is scoped to one space. Some operations require an admin key. Start with the HTTP request below to verify your workspace, then install the Node SDK for typed API calls in your application. ## Make a first request over HTTP This request runs in server-side JavaScript and needs no maxclicks package. Set `MAXCLICKS_API_KEY` in your server's secret store, then confirm the returned space before making a write. ```javascript const apiKey = process.env.MAXCLICKS_API_KEY if (!apiKey) throw new Error('Set MAXCLICKS_API_KEY first') const response = await fetch('https://api.maxclicks.ai/v1/me', { headers: { Authorization: `Bearer ${apiKey}` }, }) const result = await response.json() if (!response.ok) throw new Error(result.error?.message ?? 'Request failed') console.log(result.data.space) ``` For a runtime without `process.env`, use its server-side secret API. Keep the key out of browser bundles. Follow the [API quickstart](/quickstart) to sync a contact and send a prepared template. The examples below use the [Node SDK](/sdks/node), which provides types, pagination helpers, and typed errors. ## SDK: call maxclicks from the app Use the SDK for code that runs in the app Lovable builds: upsert contacts, fire events, send templates. ### Install the package Install the SDK in a compatible backend runtime: ```bash npm install maxclicks ``` Requires Node.js 18 or newer. Ships ESM and CommonJS builds. ### Store the API key Add `MAXCLICKS_API_KEY` in Lovable's environment variables. Never hardcode the key in client code. Call maxclicks from a server route or edge function so the key stays server-side. ### Call the API ```ts import { Maxclicks } from 'maxclicks' const mc = new Maxclicks() // reads MAXCLICKS_API_KEY // Upsert a contact by identity (id or slug of the contact schema) const contact = await mc.records.upsert('students', { email: 'ada@example.com', firstName: 'Ada', tags: ['signup'], }) // Fire an event, which can trigger a workflow or a webhook await mc.events.fire('purchase-completed', { eventId: 'ord_123', amount: 42 }) // Send a template, resolving the recipient by email await mc.templates.send(templateId, { data: { contact: { email: 'ada@example.com' } }, }) ``` On failure a call throws a typed error, one of the `MaxclicksError` subclasses (`MaxclicksNotFoundError`, `MaxclicksConflictError`, `MaxclicksRateLimitError`, and others). The SDK retries `429` within its budget; `5xx` and transport errors are retried for reads and keyed writes. Preserve the key and payload, and reconcile uncertain outcomes. See the [SDK reference](/sdks/node) for the full method list. Keep the API key on the server. A key in browser code is exposed to every visitor. Route SDK calls through a Lovable backend route or Supabase edge function. ## MCP: give Lovable's agent write access The [MCP server](/ai/mcp) turns maxclicks into tools an MCP client can call. Point Lovable's agent at it and the agent can manage contacts and objects, fire events, send templates, manage webhooks, and trigger workflows while it builds. ### Hosted (remote) Connect to `https://mcp.maxclicks.ai/mcp`. A client that supports MCP sign-in opens maxclicks so you can choose the workspace and approve what it may do; otherwise send your API key in the `Authorization: Bearer` header. Verify the connected workspace with `whoami` before making a write. ### Local (stdio) For a client that supports local stdio, use `npx -y maxclicks-mcp` with `MAXCLICKS_API_KEY` in its environment. See the [local configuration](/ai/mcp#run-it). Cloud-hosted agents need a reachable remote MCP endpoint; a local laptop process is only available to clients running on that machine. ### Tools The agent can read your schemas and attributes but not create them. Call `list_schemas` and `list_attributes` to learn how your data is shaped, then write. | Area | Tools | | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | Meta | `whoami` | | Schemas and attributes (read) | `list_schemas`, `get_schema`, `list_attributes` | | Records | `list_records`, `get_record`, `create_record`, `upsert_record`, `update_record`, `delete_record`, `get_contact_audit_trail` | | Events | `fire_event`, `fire_events_batch`, `list_events` | | Email | `send_template` | | Suppressions (admin key) | `create_suppression`, `delete_suppression` | | Webhooks | `list_webhooks`, `get_webhook`, `create_webhook`, `update_webhook`, `delete_webhook`, `rotate_webhook_secret` | | Workflows | `trigger_workflow` | ## Related * [maxclicks Node SDK](/sdks/node) * [MCP server](/ai/mcp) * [Contacts](/contacts) --- # Use maxclicks in Replit Source: https://docs.maxclicks.ai/ai/guides/replit # Use maxclicks in Replit Connect your Replit project to customer records, events, and prepared campaigns in maxclicks. Store the key as a secret and make API requests from your server runtime. ## Make a first request over HTTP This request runs in server-side JavaScript and needs no maxclicks package. Set `MAXCLICKS_API_KEY` in your server's secret store, then confirm the returned space before making a write. ```javascript const apiKey = process.env.MAXCLICKS_API_KEY if (!apiKey) throw new Error('Set MAXCLICKS_API_KEY first') const response = await fetch('https://api.maxclicks.ai/v1/me', { headers: { Authorization: `Bearer ${apiKey}` }, }) const result = await response.json() if (!response.ok) throw new Error(result.error?.message ?? 'Request failed') console.log(result.data.space) ``` For a runtime without `process.env`, use its server-side secret API. Keep the key out of browser bundles. Follow the [API quickstart](/quickstart) to sync a contact and send a prepared template. The examples below use the [Node SDK](/sdks/node) for application code and the [CLI](/ai/cli) for terminal automation. ## Get an API key Create a key in the maxclicks app under Settings, Developers (`https://app.maxclicks.ai/space/-/settings/developers`). A key is scoped to one [space](/workspace) and looks like `max_...`. The suppression endpoints are the only ones that need a key whose owner has admin permission on the space. ## Store the key as a Secret ### Open Secrets In the Replit left sidebar, open the Secrets tool (the padlock icon). ### Add the key Add a secret named `MAXCLICKS_API_KEY` with your `max_...` value. Both the SDK and the CLI read this variable by default, so no key appears in your source. Do not paste the key into a file or a Repl that is public or forked. Secrets are injected as environment variables at run time and are not committed. ## Option 1: Node SDK Use the SDK for application code. It ships TypeScript types, automatic pagination, typed errors, and bounded retries: `429` on all calls, and `5xx`/transport failures for reads and keyed writes. Install the SDK from the Replit Shell. Requires Node.js 18 or newer: ```bash npm install maxclicks ``` Construct the client with no argument to read `MAXCLICKS_API_KEY` from the Secret, then call a method: ```ts {{ title: 'ESM' }} import { Maxclicks } from 'maxclicks' const mc = new Maxclicks() // reads MAXCLICKS_API_KEY const me = await mc.me() console.log(me.space?.name) const contact = await mc.records.upsert('students', { email: 'ada@example.com', firstName: 'Ada', tags: ['beta'], }) console.log(contact.id) ``` ```js {{ title: 'CommonJS' }} const { Maxclicks } = require('maxclicks') const mc = new Maxclicks() mc.me().then((me) => console.log(me.space?.name)) ``` Pass options to the constructor to override defaults: ```ts const mc = new Maxclicks({ apiKey: process.env.MAXCLICKS_API_KEY, baseUrl: 'https://api.maxclicks.ai/v1', timeoutMs: 60000, maxRetries: 2, }) ``` On success a call returns the data directly. On failure it throws a typed error, one of the `MaxclicksError` subclasses. See the [Node SDK reference](/sdks/node) for the full method list. ## Option 2: CLI Install the CLI from the Replit Shell: ```bash npm install -g maxclicks-cli maxclicks whoami ``` It prints JSON to stdout and errors to stderr for shell scripts and tools such as `jq`. The CLI reads `MAXCLICKS_API_KEY` from the Secret. Confirm the key and its space with `whoami`, then run commands: ```bash maxclicks schemas list --type contact maxclicks records upsert students --data '{"email":"ada@example.com","firstName":"Ada"}' maxclicks events fire purchase-completed --data '{"eventId":"ord_1","amount":42}' maxclicks api GET /schemas ``` `--all` requests list pages as a flat array, subject to the API's traversal limit; it does not guarantee a full workspace export. Run `maxclicks --help` for options. See the [CLI reference](/ai/cli) for every command. ## Which to use | Use | Choose | | -------------------------------------------------- | ----------- | | A first request without a maxclicks package | Direct HTTP | | Application code with types and pagination helpers | Node SDK | | Shell scripts and JSON pipelines | CLI | ## Related * [Node SDK reference](/sdks/node) * [CLI reference](/ai/cli) * [Agent skills](/ai/skills) * [Authentication](/api/authentication) --- # Use maxclicks in v0 Source: https://docs.maxclicks.ai/ai/guides/v0 # Use maxclicks in v0 Connect the app you build in v0 to customer data and prepared campaigns in maxclicks. In a Next.js project, make API requests from server code such as a Route Handler or Server Action. The API authenticates with a workspace key. Keep it in server-side environment variables and make requests from a Route Handler or Server Action. Never expose it to the browser. Start with the HTTP request below to verify your workspace, then install the Node SDK for typed API calls in your application. ## Make a first request over HTTP This request runs in server-side JavaScript and needs no maxclicks package. Set `MAXCLICKS_API_KEY` in your server's secret store, then confirm the returned space before making a write. ```javascript const apiKey = process.env.MAXCLICKS_API_KEY if (!apiKey) throw new Error('Set MAXCLICKS_API_KEY first') const response = await fetch('https://api.maxclicks.ai/v1/me', { headers: { Authorization: `Bearer ${apiKey}` }, }) const result = await response.json() if (!response.ok) throw new Error(result.error?.message ?? 'Request failed') console.log(result.data.space) ``` For a runtime without `process.env`, use its server-side secret API. Keep the key out of browser bundles. Follow the [API quickstart](/quickstart) to sync a contact and send a prepared template. The examples below use the [Node SDK](/sdks/node), which provides types, pagination helpers, and typed errors. ## Connect ### Install the SDK Requires Node.js 18 or newer. Install the SDK in your project: ```bash npm install maxclicks ``` ### Set the API key Add `MAXCLICKS_API_KEY` to your v0 project's environment variables. Create the key in maxclicks under Settings, Developers, API keys. It looks like `max_...`. ```bash MAXCLICKS_API_KEY=max_... ``` ### Call the API from server code Construct the client once. With no arguments it reads `MAXCLICKS_API_KEY` and uses `https://api.maxclicks.ai/v1`. ```ts import { Maxclicks } from 'maxclicks' const mc = new Maxclicks() // reads MAXCLICKS_API_KEY ``` ## Server Action Use a Server Action to write from a form. This runs on the server, so the key stays private. ```ts 'use server' import { Maxclicks } from 'maxclicks' const mc = new Maxclicks() export async function subscribe(email: string, firstName: string) { return mc.records.upsert('students', { email, firstName, tags: ['beta'] }) } ``` `upsert` matches on contact identity (`userId` first, then `email`, then `phone`), so submitting the same person twice updates one [contact](/contacts) instead of creating a second. ## Route Handler Use a Route Handler when v0 code, or an external caller, posts JSON to an endpoint. ```ts import { Maxclicks } from 'maxclicks' const mc = new Maxclicks() export async function POST(request: Request) { const { email, firstName } = await request.json() const contact = await mc.records.upsert('students', { email, firstName }) return Response.json(contact) } ``` ## Common calls The first argument is a schema id or slug. Records are flat: base fields plus your own attribute values, all at the top level. ```ts await mc.me() // current key and space await mc.records.upsert('students', { email }) // create or update a contact await mc.events.fire('purchase-completed', { eventId: 'ord_123', amount: 42 }) await mc.templates.send(templateId, { data: { contact: { email } } }) await mc.workflows.trigger('7Qk2ZbN4x9LmR3vTpW8sHc0F', { orderId: 'A-1024' }) await mc.forms.submit(formId, { contact: { email } }) ``` Every `list` returns a `Page` that is both one page and an async iterable over all pages. ```ts for await (const record of await mc.records.list('students')) { console.log(record.id) } ``` ## Errors On success a call returns the data directly. On failure it throws a typed error, one of the `MaxclicksError` subclasses. | Error | When | | ------------------------------ | ----------------------------------------- | | `MaxclicksBadRequestError` | 400 validation failure | | `MaxclicksAuthenticationError` | 401 missing or invalid API key | | `MaxclicksPermissionError` | 403 insufficient permission | | `MaxclicksNotFoundError` | 404 resource not found | | `MaxclicksConflictError` | 409 identifier or uniqueness conflict | | `MaxclicksRateLimitError` | 429 rate limited (carries `retryAfterMs`) | | `MaxclicksServerError` | 5xx server error | The SDK retries `429` within its budget. It retries `5xx` and transport errors only for reads and writes carrying an idempotency key. Preserve the key and payload while reconciling an uncertain result. ```ts import { Maxclicks, MaxclicksConflictError } from 'maxclicks' const mc = new Maxclicks() try { await mc.records.create('students', { email: 'ada@example.com' }) } catch (error) { if (error instanceof MaxclicksConflictError) { // identity already exists; use upsert instead } } ``` The API key inherits its owner's workspace permissions. Store it only in server environment variables. Keys are issued and revoked in maxclicks under Settings, Developers, API keys, not over the API. If a key leaks, revoke it there and issue a new one. ## Related * [Node SDK reference](/sdks/node) * [MCP server](/ai/mcp) * [Agent skills](/ai/skills) * [Contacts](/contacts) --- # Use maxclicks in Windsurf Source: https://docs.maxclicks.ai/ai/guides/windsurf # Use maxclicks in Windsurf Use the [agent onboarding brief](/ai/onboarding) to connect your workspace, then add MCP tools or agent skills using the setup below. For direct HTTP requests, follow the [HTTP quickstart](/quickstart). Give Windsurf's agent the workspace context it needs to build a maxclicks integration, then verify the connected space with a read request. Create a key at `https://app.maxclicks.ai/space/-/settings/developers`. A key is scoped to one space. Set it as `MAXCLICKS_API_KEY` or send it as a bearer token. ## Option A: MCP server The maxclicks MCP server exposes API operations as tools. Connect over remote HTTP or run the npm package locally. ### Hosted Connect to `https://mcp.maxclicks.ai/mcp`. A client that supports MCP sign-in opens maxclicks so you can choose the workspace and approve what it may do; otherwise configure it to send your API key in its `Authorization: Bearer` header. Verify `whoami` before any write. ### Local (stdio) Requires Node.js 18 or newer. Configure the client to run the npm package with your API key: ```json { "mcpServers": { "maxclicks": { "command": "npx", "args": ["-y", "maxclicks-mcp"], "env": { "MAXCLICKS_API_KEY": "YOUR_API_KEY" } } } } ``` Keep this configuration out of version control. Confirm the host can start the local process and successfully call `whoami`. ### Tools The MCP server exposes schema and attribute reads, so use `list_schemas` and `list_attributes` to inspect the model before writing records. Create and configure those definitions in the app. Use the [API reference](/api) for request fields, response states, and permissions. | Group | Tools | | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | Meta | `whoami` | | Schemas, attributes (read) | `list_schemas`, `get_schema`, `list_attributes` | | Records | `list_records`, `get_record`, `create_record`, `upsert_record`, `update_record`, `delete_record`, `get_contact_audit_trail` | | Events | `list_events`, `fire_event`, `fire_events_batch` | | Email | `list_templates`, `get_template`, `send_template` | | Suppressions | `list_suppressions`, `create_suppression`, `delete_suppression`, `batch_create_suppressions`, `batch_delete_suppressions` | | Webhooks | `list_webhooks`, `get_webhook`, `create_webhook`, `update_webhook`, `delete_webhook`, `rotate_webhook_secret` | | Workflows | `list_workflows`, `get_workflow`, `trigger_workflow`, `pause_workflow`, `unpause_workflow`, `list_workflow_runs`, `get_workflow_run` | Segments, broadcasts, domains, senders, and topics add their own tools: mostly reads, plus creating a segment, updating a broadcast's name, audience or schedule, and sending it. Events are immutable once fired. The [MCP reference](/ai/mcp) lists them all. `create_suppression` and `delete_suppression` require an admin-scoped key. ## Option B: maxclicks skills Skills teach the data model, client interfaces, and common integration tasks. Install them with: ```bash npx skills add https://maxclicks.ai ``` See [Agent Skills](/ai/skills) for setup and the full catalog. The skill catalog covers these areas: | Skill | Loads for | | -------------------------- | ------------------------------------------------------------------------------------- | | `maxclicks` | Auth, the data model, the CLI/SDK/API map, and top gotchas. Entry point for any task. | | `maxclicks-contacts` | Creating, updating, and deleting records. | | `maxclicks-events` | Firing and listing events. | | `maxclicks-email` | Sending templates and managing suppressions. | | `maxclicks-templates` | What a template expects, who receives it, and which sender it uses. | | `maxclicks-adaptyle` | Writing template content in Adaptyle over Liquid, MJML, and HTML. | | `maxclicks-segments` | Building contact filters from a plain description. | | `maxclicks-broadcasts` | Scheduling and sending broadcasts, and reading their results. | | `maxclicks-deliverability` | Sending domains and senders, verified in the app and read over the API. | | `maxclicks-topics` | Consent topics and how sends respect them. | | `maxclicks-imports` | The app's bulk CSV import of records. | | `maxclicks-workflows` | Triggering workflows and observing runs. | | `maxclicks-webhooks` | Managing outgoing webhooks and verifying signatures. | For terminal operations, install the [CLI](/ai/cli#install) and configure your API key. Skills provide instructions; the CLI or MCP connection gives the agent workspace access. ## Which to use Use MCP for callable tools and skills for integration guidance. Add the CLI for terminal commands, or choose an [SDK](/sdks) for application code. The [agent onboarding brief](/ai/onboarding) helps you verify your workspace before making a write. --- # MCP server Source: https://docs.maxclicks.ai/ai/mcp # MCP server Give an MCP-compatible assistant the tools to work with your maxclicks customer data, events, and prepared campaigns. The server wraps the [Public API](/api); its actions use the same workspace permissions and limits as your own integration. Every call is tied to one workspace. A remote client either signs in through maxclicks, where you choose the workspace and what the assistant may do, or carries an API key created in that workspace. See [Authentication](/api/authentication). ## Current API coverage The tools use the [Public API](/api) for customer records, events, prepared campaigns, workflows, and webhooks. Follow each operation’s response and status fields: an accepted request can still require event readiness checks, deletion polling, or delivery tracking. ## Run it Connect a remote client to the hosted server, or run the npm package on your own machine. Remote clients can sign in; the local stdio transport uses an API key. ### Local (stdio) Requires Node.js 18 or later. Add the server to your MCP client and supply the workspace key in its environment: ```json { "mcpServers": { "maxclicks": { "command": "npx", "args": ["-y", "maxclicks-mcp"], "env": { "MAXCLICKS_API_KEY": "YOUR_API_KEY" } } } } ``` `MAXCLICKS_API_KEY` is required in stdio mode. The server exits if it is unset. Keep the client configuration containing your key out of version control. ### Remote (HTTP) Connect your client to `https://mcp.maxclicks.ai/mcp`. A client that supports MCP authorization signs in on its own: the server answers its first request with `401` and names the maxclicks authorization server, the client identifies itself with its [metadata document](/api/authentication#connect-an-app-with-o-auth) or by registering, and your browser opens maxclicks so you can [approve the connection](#approve-a-connection). ```bash {{ title: 'Claude Code' }} claude mcp add --transport http maxclicks https://mcp.maxclicks.ai/mcp ``` In Claude Code, run `/mcp`, select the server and authenticate. Clients configured with JSON take the URL alone: ```json {{ title: 'Cursor' }} { "mcpServers": { "maxclicks": { "url": "https://mcp.maxclicks.ai/mcp" } } } ``` A client that cannot sign in can send a workspace API key instead, as `Authorization: Bearer YOUR_API_KEY`. To run your own server with HTTP transport: ```bash npx -y maxclicks-mcp --http --host 127.0.0.1 ``` The local endpoint is `http://127.0.0.1:7004/mcp`. Override the port with `PORT` or `--port`, use `MAXCLICKS_BASE_URL` to select a non-default API host, and set `MCP_PUBLIC_URL` to the address clients use, since an OAuth token is issued for that exact address. Verify a successful `whoami` response to confirm the connected workspace before making a write. ## Approve a connection When a client signs in, maxclicks shows a consent page in your browser. Sign in first if you are not already. * The page names the app as it describes itself and marks it **Not verified**: any app can claim any name. An app identified by its metadata document also shows **Details from** with the site that published it. The page also shows where it returns you, a website's host, an app running on your computer, or an app installed on your device. Approve only a connection you started from that app just now. * Choose one workspace. Connecting requires the **Write** permission in it, as creating an API key does. * Choose what the app may do. **Read**, **Write** and **Admin** are offered only where the app asked for them and you hold them. Admin manages suppressions; assistants ask for Read and Write unless they need more. * Click **Allow access**, or **Deny**. After allowing, your browser returns to the app, which finishes connecting on its own. The connection is listed under **Settings**, then **Developers**, on the **API keys** tab, with the app's name and a code such as `OAuth · claude.ai`. Delete it there to disconnect the app at once. A connection acts as you in its one workspace, limited to what you granted and to your current role. To use another workspace, connect again and choose it. The app holds an access token valid for one hour and renews it with a refresh token. A connection left unused for 30 days expires, and the app asks you to sign in again. ## Authentication The HTTP transport authenticates every request, including requests inside an open session. It reads the credential from, in order: 1. The `Authorization: Bearer ` header: an OAuth access token issued for this server, or an API key. 2. The `X-Maxclicks-API-Key` header: an API key. A credential in the URL is not accepted. A missing or invalid credential returns `401` with a challenge that tells an OAuth client where to sign in: ```http WWW-Authenticate: Bearer resource_metadata="https://mcp.maxclicks.ai/.well-known/oauth-protected-resource/mcp", scope="read write" ``` An access token issued for the Public API rather than this server is refused with `error="invalid_token"`. A session stays bound to the credential that opened it: another credential cannot use it, and the client's refreshed token continues the same session. The server checks a credential with a `me()` call and remembers the result for up to a minute, never past the token's expiry. The stdio transport reads the key from the `MAXCLICKS_API_KEY` environment variable only. Suppression tools require the `admin` permission: an admin's API key, or a connection granted Admin. Use `whoami` to confirm which space and permissions a credential resolves to. ## Tools Tools are grouped by resource. The read tools (`list_*`, `get_*`, `count_*`, `whoami`) never change anything. The rest map to the Public API's create, update, delete, and action endpoints. The split follows the API itself: you build things in the app, and the tools trigger, read, and measure them. Schemas, attributes, templates, sending domains, senders, and topics are created and edited in the app, so their tools here are read-only. A broadcast is drafted and its email written in the app too, but `update_broadcast` can still rename one, point its audience at a different segment or at every contact in its schema, and change when it goes out. | Group | Tools | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Meta | `whoami` | | Schemas | `list_schemas`, `get_schema` | | Attributes | `list_attributes` | | Records | `list_records`, `get_record`, `create_record`, `upsert_record`, `update_record`, `delete_record`, `get_record_deletion`, `cancel_record_deletion`, `get_contact_audit_trail` | | Events | `fire_event`, `fire_events_batch`, `get_event_status`, `list_events` | | Templates | `list_templates`, `get_template`, `send_template` | | Broadcasts | `list_broadcasts`, `get_broadcast`, `update_broadcast`, `send_broadcast`, `list_broadcast_runs`, `get_broadcast_metrics` | | Segments | `list_segments`, `get_segment`, `create_segment`, `delete_segment`, `count_segment`, `list_segment_contacts` | | Topics | `list_topics`, `get_topic` | | Senders | `list_senders` | | Domains | `list_domains`, `get_domain` | | Suppressions | `list_suppressions`, `create_suppression`, `delete_suppression`, `batch_create_suppressions`, `batch_delete_suppressions` | | Webhooks | `list_webhooks`, `get_webhook`, `create_webhook`, `update_webhook`, `delete_webhook`, `rotate_webhook_secret` | | Workflows | `list_workflows`, `get_workflow`, `pause_workflow`, `unpause_workflow`, `trigger_workflow`, `list_workflow_runs`, `get_workflow_run` | ### Discover, then write Start with `list_schemas` and `list_attributes` so the agent learns how your data is shaped, then read and write [records](/contacts). Every record tool takes a schema id or slug, and acts on the contacts or objects in that schema. `upsert_record` (tool) Create or update by the schema's identity rules. An explicit `id` selects one record; otherwise `userId` takes priority, email matches only without `userId`, and phone matches only without `userId` or email. Preserve your identity strategy and inspect warnings. `fire_events_batch` (tool) Accept up to 500 events in one schema. Matching `eventId` and normalized input replay the original receipt; changed input conflicts. Inspect every batch result and follow the [event readiness flow](/events) before relying on downstream work. `send_template` (tool) Send a [template](/templates) to a contact, passing in the information that template asks for. `trigger_workflow` (tool) Start a run of a [workflow](/workflows) that has an incoming-webhook trigger. `create_segment` (tool) Build a [segment](/segments) from a plain description of the audience. maxclicks writes the filter and stores it. `send_broadcast` (tool) Send or schedule a [broadcast](/broadcast) that was drafted in the app, then read `list_broadcast_runs` and `get_broadcast_metrics` to measure it. ## Related * [Authentication](/api/authentication): API keys, and how an app connects with OAuth. * [Node SDK](/sdks/node): the `maxclicks` client this server is built on. * [Public API](/api): the endpoints every tool maps to. --- # Onboard an AI agent Source: https://docs.maxclicks.ai/ai/onboarding # Onboard an AI agent Give your coding agent the current API reference and a workspace API key. Start with a read request, then connect one customer record. You do not need an SDK, CLI, MCP server, or skill package for this path. ## Get an API key In your space, open **Settings → Developers → API keys** and create a key. Store the value in your agent environment as `MAXCLICKS_API_KEY`; keep it out of source files and browser code. The key inherits its owner's permissions. See [authentication](/api/authentication). Ask your agent to use this brief: > Use the maxclicks API reference at and its HTTP quickstart. Read MAXCLICKS\_API\_KEY from the environment. First call GET /v1/me and confirm the workspace. Then list schemas and read the contact schema's attributes. Show me the fields and requirements before writing a record. Do not send email or trigger a workflow as part of this setup. ```bash curl --fail-with-body https://api.maxclicks.ai/v1/me \ -H "Authorization: Bearer $MAXCLICKS_API_KEY" ``` The response should identify your intended workspace in `data.space`. Continue with the [HTTP quickstart](/quickstart) to upsert a test contact and send a prepared template. Give the agent the [error](/api/errors) and [idempotency](/api/idempotency) references when you add writes. ## Install the agent skills Install the skills to give your agent the maxclicks data model and integration conventions: ```bash npx skills add https://maxclicks.ai ``` Start with the `maxclicks` skill, then add the skills for the areas you are working with. See [Agent Skills](/ai/skills) for the full catalog. ## Connect the MCP server Use MCP to give your assistant callable tools. Connect to `https://mcp.maxclicks.ai/mcp` and approve the connection when maxclicks opens in your browser, or send an API key; in a client that supports local stdio, use `npx -y maxclicks-mcp` with your key. The [MCP reference](/ai/mcp) includes configuration and the tool list. After connecting, call `whoami`, `list_schemas`, and `list_attributes`. These establish the workspace and data model before any write. ## Use the CLI Install the CLI for terminal automation: ```bash npm install -g maxclicks-cli maxclicks login maxclicks whoami ``` For an agent environment, set `MAXCLICKS_API_KEY` instead of using interactive login. The [CLI reference](/ai/cli) documents configuration, commands, and JSON output. For application code, choose an [SDK](/sdks) for your language. --- # Agent Skills Source: https://docs.maxclicks.ai/ai/skills # Agent Skills Give your coding agent the maxclicks data model and API conventions it needs to build an integration. Agent skills provide task-specific instructions; API credentials and tools provide access to your workspace. For a first integration, follow [agent onboarding](/ai/onboarding). Install the skills below to give your agent task-specific guidance alongside the [API reference](/api). ## Install Install the skills into your coding agent: ```bash npx skills add https://maxclicks.ai ``` The package supports skills-compatible coding agents. Follow your host’s skill-loading instructions, and update installed skills with `npx skills update -y`. For manual installation, copy the relevant `skills//` directory from the `maxclicks-ai` repository into your agent’s supported skills directory. ## API compatibility Skills work with the [Public API](/api), the [CLI](/ai/cli), [SDKs](/sdks), and [MCP server](/ai/mcp). Give your agent the API reference for request fields, response states, permissions, and idempotency behavior. Skills provide guidance; your configured client and API key provide workspace access. ## Prerequisites * A workspace [API key](/api/authentication), stored in the agent's environment as `MAXCLICKS_API_KEY`. * The [API reference](/api) for request and response details. * A configured [CLI](/ai/cli), [SDK](/sdks), [MCP server](/ai/mcp), or HTTP client. ## The skills Start with the `maxclicks` skill. It is the entry point, it loads on any maxclicks task, and every other skill builds on it. Add the skill for whichever area you are working in. The skills inherit the line the API draws: you build things in the app, and an agent triggers, reads, and measures them. [Schemas](/schemas), [attributes](/attributes), [templates](/templates), sending [domains](/domain), senders, topics, and API keys are all created and edited in the app, and so is a broadcast and the email inside it, though an agent can still rename a broadcast, change its audience or its schedule, and send it. | Skill | Covers | | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `maxclicks` | Entry point: auth, the schema-based data model, the CLI/SDK/API map, and the top gotchas. | | `maxclicks-contacts` | Create, update, list, or delete [contacts](/contacts) and [objects](/objects) (records), and read a contact audit trail. | | `maxclicks-events` | Fire and list [events](/events) to trigger workflows and webhooks. | | `maxclicks-email` | Send a stored [template](/templates) to a contact, and manage email suppressions. | | `maxclicks-templates` | Before a send, read a template and what it needs: the information it expects (`expectedDataRepresentation`), who receives it, and which sender it uses. Writing the template itself happens in the app. | | `maxclicks-adaptyle` | Write a template's subject and body in [Adaptyle](/adaptyle) over Liquid, with MJML or HTML raw bodies, in the app's [editor](/editor). | | `maxclicks-segments` | Build [segments](/segments): contact filters that AI writes from a plain description. | | `maxclicks-broadcasts` | Schedule and send a [broadcast](/broadcast) drafted in the app, then read its runs and metrics. | | `maxclicks-deliverability` | Read sending [domains](/domain) and saved senders, and check whether a domain is verified. Registering and verifying a domain happens in the app, because it claims the name and sets up the sending resources behind it. | | `maxclicks-topics` | Read communication topics to pick the `communicationTopicId` for a consent-scoped marketing send. Topics are managed in the app. | | `maxclicks-imports` | Bulk CSV import of contacts and objects in the app. | | `maxclicks-workflows` | Trigger a [workflow](/workflows) through its incoming-webhook step, and observe runs. | | `maxclicks-webhooks` | Create and manage outgoing [webhooks](/webhooks), and verify inbound signatures. | ## The agent | Agent | Use it for | | -------------------------------- | -------------------------------------------------------------- | | `maxclicks-integration-engineer` | Building or debugging a full maxclicks integration end to end. | ## Related * [maxclicks CLI](/ai/cli) * [MCP server](/ai/mcp) * [SDKs](/sdks) --- # API reference Source: https://docs.maxclicks.ai/api # API reference Connect the customer data in your product to the audiences, campaigns, and journeys your team builds in maxclicks. The REST API lets you keep records current, send events, deliver prepared emails, and bring results back into your own systems. Your team sets the brand, data model, content, and journeys in the app. Your code connects them to what each customer does. Requests use JSON over HTTPS, under the `/v1` path. ## Start with the outcome | You want to… | Use | | ------------------------------------------------------- | ----------------------------------------------------------------------------------- | | Connect your first service | [API quickstart](/quickstart) and [authentication](/api/authentication) | | Keep customer and business data current | [Schemas](/api/schemas), [attributes](/api/attributes), and [records](/api/records) | | Respond to a purchase, signup, or other customer action | [Events](/api/events) and [workflows](/api/workflows) | | Build an audience from a description | [Segments](/api/segments) | | Send a prepared email or campaign | [Templates](/api/templates) and [broadcasts](/api/broadcasts) | | Keep delivery preferences in sync | [Suppressions](/api/suppressions) and [topics](/api/topics) | | Receive changes in your own system | [Webhooks](/api/webhooks) | Schemas, attributes, templates, broadcasts, domains, senders, topics, and API keys are created in the app. Use their reference pages to see which read, update, and send operations the API supports. ## Base URL ```text https://api.maxclicks.ai/v1 ``` All examples use this production URL. Configure the base URL in one place in your integration. ## Authentication Create a key in your space under **Settings → Developers → API keys**. Store it in your server's secret manager or `MAXCLICKS_API_KEY` environment variable, then verify it with a read request: ```bash curl https://api.maxclicks.ai/v1/me \ -H "Authorization: Bearer $MAXCLICKS_API_KEY" ``` `GET /v1/me` returns the key's masked identity, owner, bound space, and role. It does not change customer data. Confirm the space before making a write. A key without a space binding returns `space: null` and `role: null`; see [space resolution](/api/authentication#space-resolution). ## Response envelope Successful responses put the result in `data`. Check the HTTP status and then the operation's result: accepting a request does not always mean a message was delivered. ```json { "data": { "id": "con_BcwDvBUeSaSDILA5tHgpmU7I", "email": "sarah@example.com" } } ``` Responses can also include a top-level `warnings` array. Log these messages: they explain adjustments such as a clamped page size or an identifier update that was skipped. List endpoints also return `pagination`. ## Object ids Treat maxclicks IDs as opaque, case-sensitive strings. Current IDs can include model prefixes such as `con_` for a contact and `tpl_` for a template. Store the full value returned by the API and send it back unchanged; do not generate or parse it yourself. Your own identifiers have a different purpose: | Field | Resource | Use it for | | ------------ | -------- | -------------------------------------------- | | `userId` | Contacts | Your product's customer or user ID | | `externalId` | Objects | The record's ID in your source system | | `eventId` | Events | Your deduplication identifier for that event | ## Pagination Most lists accept `limit` and `offset` and return `pagination.hasMore`. The default page size is `50`, usually capped at `200`; offsets above `10000` return `400 offset_too_large`. Advance using the **returned** `pagination.limit`, since some endpoints apply a lower cap. `GET /v1/events` uses a cursor. Pass `pagination.nextCursor` back as `cursor`, keeping the same schema and time range. See [pagination](/api/pagination) for both response shapes and iteration examples. ## Error envelope Standard failures return an `error` object. An aborting event batch instead returns its rollback results under `data` with HTTP `422`. Branch on the HTTP status and `error.code`, and use `error.message` for diagnostics. ```json { "error": { "type": "invalid_request_error", "code": "identifier_conflict", "message": "A record with this identifier already exists." } } ``` For standard errors, `type` is `invalid_request_error` on `4xx` and `api_error` on `5xx`. `code` can be `null`; validation failures may include `issues`. Clients should tolerate extra fields and a missing `type` on an unresolved-operation response. The [error guide](/api/errors) explains what to correct, retry, or reconcile. ## Rate limits Authenticated requests share three buckets per API key: | Bucket | Limit | Applies to | | ------- | ------------------- | ----------------------------------------------------------------- | | `read` | 100 requests/second | Read endpoints | | `write` | 25 requests/second | Writes outside the AI bucket | | `ai` | 10 requests/minute | Segment generation; webhook writes with a custom filter condition | On `429`, respect `Retry-After` and add jitter before retrying. Handle both HTTP dates and delay seconds. Public form endpoints have separate limits. See [rate limits](/api/rate-limit). ## Idempotency Use a fresh `Idempotency-Key` for each logical write on a [supported endpoint](/api/idempotency#supported-endpoints). Keep that key and the request body when retrying the same operation. A completed result can be replayed; an unresolved result must be reconciled before you start another operation. A timeout or `5xx` does not prove that a write failed before taking effect. Do not switch to a new key to force it through. See [safe retries and unresolved outcomes](/api/idempotency). ## Write only fields your schema accepts Record and event inputs are flat JSON objects: platform fields and custom attribute keys sit at the top level. Read [attribute definitions](/api/attributes) first. A writable field has `definition.type` other than `evaluated` and no `definition.valueSource`. Aggregates keep a stored type and use `valueSource.type: "aggregate"`; sending an aggregate value does not update its source data. Use JSON numbers for `number` and booleans for `boolean`. Send `decimal` as an exact string such as `"42.50"`: writable decimals allow up to 15 integer digits and warn beyond five fractional digits. Use `YYYY-MM-DD` for `date only`, `HH:mm:ss.sss` for `time only`, and an ISO 8601 timestamp for `date time`. Structured fields use JSON arrays/objects. Platform timestamps are UTC ISO 8601 strings. Use the schema definition's nullable, options, formats, and JSON schema to validate inputs; omission and explicit `null` have different update meanings. Public record reads return stored data and platform fields. They have no evaluated/aggregate expansion flag and no ad-hoc record search query. ## Accept an event, then check readiness Send a business occurrence to an event schema with a stable `eventId`. Explicitly link `contactId` or `objectId` when the occurrence belongs to an existing subject; only one may be supplied. An email inside custom event data does not create that subject link. ```bash curl -X POST https://api.maxclicks.ai/v1/events/purchase-completed \ -H "Authorization: Bearer $MAXCLICKS_API_KEY" \ -H "Content-Type: application/json" \ -d '{"eventId":"order-1042-paid","contactId":"YOUR_CONTACT_ID","amount":42}' ``` Replace the schema, contact, and custom fields with your configured values. A `202` response gives an immutable receipt: ```json { "data": { "accepted": 1, "id": "ev_BcwDvBUeSaSDILA5tHgpmU7I", "eventId": "order-1042-paid", "isDuplicate": false, "readinessAtAcceptance": "pending" } } ``` Missing AI auto-fill values can require asynchronous preparation. Save the receipt, then call `GET /v1/events/{schema}/{id}/status` with the **maxclicks `id`**. `readinessState` can be `pending`, `ready`, `failed`, or `outcome_unknown`. Ready means prepared for consumers; it does not confirm completion of a workflow or email. Failed or unresolved preparation needs investigation; a new `eventId` would create another occurrence. Retry a lost acceptance with the same `eventId`, normalized payload, and explicit `occurredAt` if provided. The same occurrence returns its original receipt with `isDuplicate: true`; its original `readinessAtAcceptance` does not change when preparation finishes. Changed input conflicts. Normalized inputs are capped at 256 KiB, and an explicit `occurredAt` must be within the previous 366 days or five minutes into the future. For [batches](/api/events#events.fireBatch), `onError: "continue"` commits each accepted item independently and returns per-item failures, even with HTTP `200`. `onError: "abort"` rolls back new acceptances on the first failure, marks earlier items `rolled_back`, and returns HTTP `422` with a `data` result. Check every item before advancing your source checkpoint. [Admission limits](/api/rate-limit#event-admission) apply per occurrence. Browser observations use an enabled [Event collection](/events#collected-events) binding with a source and subject schema. To bind an anonymous source identity to a contact, use [event identity changes](/api/events#eventIdentities.identify) from a server. All six fields are required; keep the per-alias revision as a string and reuse the UUID operation ID on retries. A binding must be enabled for that source and contact schema. Do not send your public API key from a browser. ## Read the operation's outcome | Action | What the immediate result establishes | What to inspect next | | ------------------ | ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- | | Fire an event | Durable acceptance and its original readiness | [Current readiness](/api/events#events.status), then downstream runs | | Trigger a workflow | A run was created; `data` is an empty object | [Workflow runs and history](/api/workflows#workflows.listRuns) | | Send a broadcast | Scheduling was accepted | [Broadcast runs](/api/broadcasts#broadcasts.listRuns); the app exposes fuller planning/quality state | | Send a template | Synchronous send result, possibly `status: "failed"` on HTTP `200` | Result status/error, then delivery events | | Delete a record | Deletion operation ID and current state | [Deletion status](/api/records#records.getDeletion) until `completed` | Record deletion returns HTTP `202` while cleanup is unfinished and HTTP `200` when completed. Keep the entire `operationId` and poll its status; pending means cleanup is unfinished, and blocked means unresolved work requires reconciliation. Acceptance does not establish completed erasure. Deletion requires an explicit `Idempotency-Key` for the attempt. During admission drain, the original requester with current write access can [request cancellation](/api/records#records.cancelDeletion) before removal begins. Poll the operation until `cancelled`; cancellation cannot restore removed data. The [idempotency reference](/api/idempotency#record-deletion-attempts) distinguishes attempt identity from cached responses. ## Versioning The API version is part of the path: every endpoint is served under `/v1`. Within `/v1`, maxclicks only makes additive changes: new endpoints, new optional parameters, new response fields and new enum values. Write clients that ignore fields and values they do not recognize. A change that would break an existing request or response ships under a new path version, such as `/v2`, while `/v1` keeps working. When an endpoint, parameter or field is deprecated, responses that use it carry a `Deprecation` header (RFC 9745) with the date it was deprecated, a `Sunset` header (RFC 8594) with the date it stops working, and a `Link` header with `rel="deprecation"` pointing to the migration notes. The sunset date is at least six months after the deprecation is announced, and the change is also announced in these docs. Watch for these headers in your logs so a deprecation never surprises you. The OpenAPI document describes API version `1.1.0`, served under `/v1`. Use the same specification for request validation, generated clients, and API exploration: * [OpenAPI specification (YAML)](/api-spec/openapi.yaml) * [OpenAPI specification (JSON)](/api-spec/openapi.json) * [Client libraries and SDK guides](/sdks) ## Related * [Make your first request](/quickstart) * [Authenticate a service](/api/authentication) * [Handle errors](/api/errors) * [Page through results](/api/pagination) --- # Attributes API reference Source: https://docs.maxclicks.ai/api/attributes Read a schema's attributes. Attributes are authored in the app. Base URL: https://api.maxclicks.ai ## List attributes `GET /v1/schemas/{schema}/attributes` Lists base attributes first, then custom attributes. Base ids use base: and have null sequence and timestamps. definition.type uses the documented stored/evaluated types; aggregate is definition.valueSource.type, not a top-level type. Only definitions with type other than evaluated and no valueSource are writable. Definition metadata can include auto-fill settings, aggregate source configuration, or evaluated target/reference descriptions; generated expressions are excluded. Record GET/list does not expand evaluated or aggregate values. Attribute authoring is available in the app, not through public v1. Authentication: Bearer API key required. ### Parameters - `schema` (path, string, required): A schema id or slug. - `limit` (query, integer, optional): Page size. Clamped to 1..200 with a warning if out of range. - `offset` (query, integer, optional): Rows to skip. Negative values clamp to 0 with a warning; above 10000 returns 400 offset_too_large. pagination.hasMore becomes false when the next offset reaches the cap even if totalCount is larger. Records list has no ad-hoc filter; this is not an unlimited export contract. ### Response example ```json { "data": [ { "id": "attr_BcwDvBUeSaSDILA5tHgpmU7I", "key": "string", "label": "string", "description": "string", "sequence": "string", "definition": { "type": "string", "nullable": false, "multiLine": false, "format": "string", "jsonSchema": {}, "autoFill": {}, "valueSource": {}, "target": "schema", "cardinality": "one", "schemaId": "sch_BcwDvBUeSaSDILA5tHgpmU7I" }, "integrationVendorName": "string", "createdAt": "2025-01-15T10:30:00.000Z", "updatedAt": "2025-01-15T10:30:00.000Z" } ], "pagination": { "limit": 0, "offset": 0, "totalCount": 0, "hasMore": false }, "warnings": [] } ``` ### Responses - 200: A page of attributes. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 404: The referenced resource was not found. - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. --- # Authentication Source: https://docs.maxclicks.ai/api/authentication # Authentication Connect your service to the right customer workspace with a Bearer API key. Create the key in maxclicks, store it on your server, and verify its space before reading or changing customer data. The base URL is `https://api.maxclicks.ai/v1`. An app that acts for a maxclicks user, such as an AI assistant, can instead [connect with OAuth](#connect-an-app-with-o-auth) and send the access token the user approves. A key is an opaque secret prefixed with `max_`. maxclicks matches it by hash, so the full value is shown once, at creation. Treat it as a password: keep it server-side, never in client code or a repository. ## Send the key Put the key in the `Authorization` header on every authenticated request. ```bash {{ title: 'cURL' }} curl https://api.maxclicks.ai/v1/me \ -H "Authorization: Bearer $MAXCLICKS_API_KEY" ``` Set `MAXCLICKS_API_KEY` in your server environment or secret manager. Send the standard `Bearer` prefix on every authenticated request. ## Verify a key `GET /v1/me` returns the calling key, its owning user, the bound space, and the caller's role. It works for space-bound and space-unbound keys, so use it to confirm a key and discover its context. Send it exactly as shown above: no body, no query parameters. The response wraps a single `data` object. `space` and `role` are `null` for a space-unbound key. ```json {{ title: 'Response: 200' }} { "data": { "apiKey": { "id": "max_BcwDvBUeSaSDILA5tHgpmU7I", "name": "Production key", "keyDisplay": "max_...3a9f", "createdAt": "2026-01-15T10:30:00.000Z" }, "user": { "id": "usr_BcwDvBUeSaSDILA5tHgpmU7I", "email": "you@yourcompany.com", "name": "Jordan Lee" }, "space": { "id": "spc_BcwDvBUeSaSDILA5tHgpmU7I", "slug": "your-company", "name": "Your Company" }, "role": { "name": "member", "label": "Member", "description": "Can edit" }, "oauth": null } } ``` For an OAuth access token, `oauth` reports the app's `clientId` (its registered id, or its metadata document URL), the resource the token was issued for, its granted scopes and its expiry, and `apiKey` describes the connection, named after the app. It is `null` for an API key. ## Roles and permissions A key acts as the membership its owning user holds in the resolved [space](/workspace). Each membership has a role, and each role grants a set of permissions: `read`, `write`, and `admin`. Endpoints are gated by one permission. | Role | Permissions | | ---------------- | ------------------------ | | `admin` | `read`, `write`, `admin` | | `non-paid admin` | `read`, `admin` | | `member` | `read`, `write` | `read`Required by all list and retrieve endpoints. `write` Required by endpoints that create, update, delete, or upsert records, fire events, manage segments and webhooks, send a stored template or a broadcast, and trigger or pause a workflow. `admin`Required by the suppression endpoints. If the resolved membership lacks the required permission, the request returns `403` with a message such as `This operation needs "write" permission.` ## Space resolution Most endpoints act on one space, resolved from the key's owner: * A **space-bound** key uses its bound space. If that space is out of the caller's memberships, the request returns `403 Space is out of scope.` * A **space-unbound** key resolves to the owner's single membership. If the owner has more than one membership, the request returns `400 unknown_space` (`Unknown space.`). * If the owner has no matching membership, the request returns `403`. `GET /v1/me` is the exception: it does not resolve a membership and never enforces a permission, so it works for any valid key. ## Manage keys Keys are created, named, and revoked in the app: open a space, go to Settings, then Developers, and use the API keys tab. Keys inherit their owner's permissions in the space. There is no key-management API. To rotate a credential, create a replacement, update your service, verify it with `GET /v1/me`, and then revoke the old key. Complete or reconcile in-flight [idempotent operations](/api/idempotency) before switching the key used to retry them. To confirm which key a running service is using, call `GET /v1/me` with it. That returns the key's masked display value, its owner, the bound space, and the role, which is enough to identify a key in the dashboard list without ever sending the plaintext back. The plaintext key is shown once, at creation, in the dialog that creates it. After that only the masked `keyDisplay` (for example `max_...ab12`) is visible, in the app and in `GET /v1/me`. Copy it into your secret store before closing the dialog. If you lose it, revoke the key and create another. ## Connect an app with OAuth An app that acts for a maxclicks user gets an access token through OAuth 2.1 instead of being handed a key. The user signs in to maxclicks, chooses one workspace and what the app may do, and the app receives a token for that connection. MCP clients do this on their own when they meet the [MCP server](/ai/mcp)'s `401`; the steps below are what any client implements. ### Discover the server The authorization server metadata (RFC 8414) lists every endpoint and capability: ```bash {{ title: 'cURL' }} curl https://api.maxclicks.ai/.well-known/oauth-authorization-server ``` A `401` from the API names its resource metadata (RFC 9728) in the `WWW-Authenticate` header, `https://api.maxclicks.ai/.well-known/oauth-protected-resource/v1`, which points at the same server. PKCE with `S256` is required, and every authorization response carries the issuer as `iss` (RFC 9207). ### Identify the app with a metadata document An app can skip registration: it publishes a JSON document about itself at an `https` URL and uses that URL as its `client_id` (OAuth Client ID Metadata Documents, which the MCP authorization specification recommends). The server metadata advertises it with `client_id_metadata_document_supported: true`, and the same `client_id` works with every server that supports it. ```json {{ title: 'https://acme.example/oauth/client.json' }} { "client_id": "https://acme.example/oauth/client.json", "client_name": "Acme assistant", "redirect_uris": ["http://127.0.0.1:8976/callback"], "grant_types": ["authorization_code", "refresh_token"], "response_types": ["code"], "token_endpoint_auth_method": "none" } ``` maxclicks reads the document when a user starts connecting, and stops with an error page that names the problem when the document cannot be used: * The `client_id` is an `https` URL with a path, written in canonical form (a lowercase host, no default port, no `.` or `..` segments), with no fragment and no credentials. The document's own `client_id` must be exactly that URL. * The URL answers `200` within 5 seconds, as `application/json` or another `+json` type, in at most 5 KB. A redirect is not followed. * The document includes `client_name` and `redirect_uris`, under the same rules as registration below. It contains no `client_secret`, and `token_endpoint_auth_method` is `none` or omitted: the app is a public client and presents its `client_id` without a secret at the token and revocation endpoints. `grant_types` and `response_types`, when present, include `authorization_code` and `code`. * A valid document is kept for its `Cache-Control: max-age`, between 5 minutes and 24 hours, or 1 hour when it sends none. A document that fails is never kept, so fix it and connect again. The consent screen shows the document's host as **Details from**, beside the app's name and where it returns. ### Register the app An app without a metadata document registers with dynamic client registration (RFC 7591). An app that cannot keep a secret sets `token_endpoint_auth_method` to `none`: ```bash {{ title: 'cURL' }} curl https://api.maxclicks.ai/oauth/register \ -H "Content-Type: application/json" \ -d '{ "client_name": "Acme assistant", "redirect_uris": ["http://127.0.0.1:8976/callback"], "token_endpoint_auth_method": "none" }' ``` ```json {{ title: 'Response: 201' }} { "client_id": "oac_...", "client_id_issued_at": 1792982615, "client_name": "Acme assistant", "redirect_uris": ["http://127.0.0.1:8976/callback"], "grant_types": ["authorization_code", "refresh_token"], "response_types": ["code"], "token_endpoint_auth_method": "none" } ``` A redirect URI is an `https` URL, an `http` URL on `localhost`, `127.0.0.1` or `[::1]`, or an app's private-use scheme, with no fragment and no credentials. A loopback redirect may use any port at authorization time. Omitting `token_endpoint_auth_method` registers a confidential client and returns a `client_secret`, which the app then presents with HTTP Basic or as `client_secret` in the form body. ### Send the user to approve Open the authorization endpoint in the user's browser: ```text {{ title: 'Authorization request' }} https://api.maxclicks.ai/oauth/authorize ?response_type=code &client_id=oac_... &redirect_uri=http://127.0.0.1:8976/callback &code_challenge=BASE64URL(SHA256(code_verifier)) &code_challenge_method=S256 &scope=read%20write &state=RANDOM_STATE &resource=https://api.maxclicks.ai/v1 ``` `client_id` is the registered `oac_...` id or the metadata document URL. `scope` lists `read`, `write` and `admin`; when omitted it means `read write`. `resource` is the server the token is for: `https://api.maxclicks.ai/v1` for this API or `https://mcp.maxclicks.ai/mcp` for the MCP server; when omitted it means the MCP server. The user signs in, chooses a workspace where they hold `write`, and grants some or all of the requested scopes. maxclicks asks every time. After approval the browser returns to `redirect_uri` with `code`, `state` and `iss`. Check that `state` is the one you sent. A code is valid for two minutes, once. A malformed request shows the user an error page instead of redirecting. A declined request returns `error=access_denied` only to an app on the user's device; a website receives nothing. ### Exchange the code ```bash {{ title: 'cURL' }} curl https://api.maxclicks.ai/oauth/token \ -d grant_type=authorization_code \ -d client_id=oac_... \ -d code=mxc_... \ -d code_verifier=YOUR_CODE_VERIFIER \ -d redirect_uri=http://127.0.0.1:8976/callback ``` ```json {{ title: 'Response: 200' }} { "access_token": "mxa_...", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "mxr_...", "scope": "read write" } ``` An app identified by a metadata document sends its document URL as `client_id`, here and when it refreshes or revokes. Send the access token the same way as an API key: `Authorization: Bearer mxa_...`. It works only at the resource it was issued for. ### Refresh and revoke Renew the access token before its hour runs out: ```bash {{ title: 'cURL' }} curl https://api.maxclicks.ai/oauth/token \ -d grant_type=refresh_token \ -d client_id=oac_... \ -d refresh_token=mxr_... ``` Every refresh returns a new refresh token. Store it and discard the old one: presenting an old refresh token again ends the connection, because it may have been copied. A refresh token unused for 30 days expires. To end a connection, revoke its refresh token (RFC 7009). The endpoint answers `200` whether or not the token existed. ```bash {{ title: 'cURL' }} curl https://api.maxclicks.ai/oauth/revoke \ -d client_id=oac_... \ -d token=mxr_... ``` The user can also disconnect the app in maxclicks: open a space, go to Settings, then Developers, and delete the connection on the API keys tab, where it is listed with the app's name and a code such as `OAuth · claude.ai`. ### What a connection can do A connection acts as the approving user in its one workspace. Its permissions are the scopes the user granted, limited again by the user's current role. When the role has a permission the connection was not granted, the request returns `403` with `code: insufficient_scope`; connect again and grant it. The OAuth endpoints answer errors the OAuth way (RFC 6749, section 5.2): `{ "error": "invalid_grant", "error_description": "..." }`, with `invalid_client` as `401`. ## Errors Authentication and authorization failures use the standard error envelope. `type` is `invalid_request_error` for `4xx` and `api_error` for `5xx`. `code` is a stable machine string, or `null` when none applies. ```json {{ title: 'Error envelope' }} { "error": { "type": "invalid_request_error", "code": "invalid_api_key", "message": "No API key provided." } } ``` | Status | `code` | When | | ------ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------ | | `401` | `invalid_api_key` | No key sent (`No API key provided.`) or the key matched none on file (`Invalid API key.`). | | `401` | `invalid_access_token` | An OAuth access token that expired or was revoked, or whose connection ended. Refresh it, or connect again. | | `403` | `insufficient_scope` | An OAuth connection whose user holds the permission but did not grant it to this connection. | | `403` | `null` | Valid key, but the membership lacks the permission, has no membership for the space, or the bound space is out of scope. | | `400` | `unknown_space` | The owner has more than one membership, so the space is ambiguous. | | `429` | `rate_limit_exceeded` | The key exceeded its rate-limit bucket. | ## Rate limits Authenticated routes are limited per key or OAuth access token (hashed `Authorization` header, falling back to client IP). There are three buckets: | Bucket | Limit | Applies to | | ------- | ------------ | --------------------- | | `read` | 100 / second | Read endpoints. | | `write` | 25 / second | Mutating endpoints. | | `ai` | 10 / minute | LLM-backed endpoints. | Exceeding a bucket returns `429` with `code: rate_limit_exceeded` and a `Retry-After` header naming when to try again. Every response from a rate-limited route reports its budgets in the `RateLimit-Policy` and `RateLimit` headers (IETF draft-ietf-httpapi-ratelimit-headers). `RateLimit-Policy` names each limit that applies, with its quota `q` and window `w` in seconds. `RateLimit` gives what remains: `r` requests until the window resets in `t` seconds. Slow down when `r` nears zero instead of waiting for a `429`. ```http {{ title: 'Response headers' }} RateLimit-Policy: "public-v1:read";q=100;w=1 RateLimit: "public-v1:read";r=99;t=1 ``` An endpoint metered in more than one way, such as the unauthenticated ones below, lists one item per limit. ## Unauthenticated endpoints Three public endpoints take no API key and resolve their tenant from the resource, not a key: | Endpoint | Purpose | | ---------------------------------------- | -------------------------------- | | `POST /v1/forms/{formId}/submit` | Anonymous form submission. | | `GET /v1/forms/{formId}/confirm/{token}` | Double opt-in confirmation link. | | `POST /v1/emails/{emailId}/unsubscribe` | RFC 8058 one-click unsubscribe. | Form endpoints use [separate limits](/api/rate-limit#form-endpoints). The one-click unsubscribe endpoint does not use a rate limiter. --- # Broadcasts API reference Source: https://docs.maxclicks.ai/api/broadcasts Schedule, send, and measure mass emails. Broadcasts are authored in the app. Base URL: https://api.maxclicks.ai ## List broadcasts `GET /v1/broadcasts` Authentication: Bearer API key required. ### Parameters - `limit` (query, integer, optional): Page size. Clamped to 1..200 with a warning if out of range. - `offset` (query, integer, optional): Rows to skip. Negative values clamp to 0 with a warning; above 10000 returns 400 offset_too_large. pagination.hasMore becomes false when the next offset reaches the cap even if totalCount is larger. Records list has no ad-hoc filter; this is not an unlimited export contract. ### Response example ```json { "data": [ { "id": "bc_BcwDvBUeSaSDILA5tHgpmU7I", "createdAt": "2025-01-15T10:30:00.000Z", "updatedAt": "2025-01-15T10:30:00.000Z", "name": "string", "description": "string", "contactSchemaId": "sch_BcwDvBUeSaSDILA5tHgpmU7I", "contactSchemaSlug": "string", "schedulingStatus": "none", "scheduledFor": "2025-01-15T10:30:00.000Z", "sendingStartedAt": "2025-01-15T10:30:00.000Z", "sendingFinishedAt": "2025-01-15T10:30:00.000Z", "failureReason": "string", "audience": { "type": "all" }, "hasContent": false } ], "pagination": { "limit": 0, "offset": 0, "totalCount": 0, "hasMore": false }, "warnings": [] } ``` ### Responses - 200: A page of broadcasts. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. ## Get a broadcast `GET /v1/broadcasts/{id}` Error: `broadcast_not_found`. Authentication: Bearer API key required. ### Parameters - `id` (path, string, required): The broadcast id. ### Response example ```json { "data": { "id": "bc_BcwDvBUeSaSDILA5tHgpmU7I", "createdAt": "2025-01-15T10:30:00.000Z", "updatedAt": "2025-01-15T10:30:00.000Z", "name": "string", "description": "string", "contactSchemaId": "sch_BcwDvBUeSaSDILA5tHgpmU7I", "contactSchemaSlug": "string", "schedulingStatus": "none", "scheduledFor": "2025-01-15T10:30:00.000Z", "sendingStartedAt": "2025-01-15T10:30:00.000Z", "sendingFinishedAt": "2025-01-15T10:30:00.000Z", "failureReason": "string", "audience": { "type": "all" }, "hasContent": false }, "warnings": [] } ``` ### Responses - 200: The broadcast. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 404: The referenced resource was not found. - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. ## Update a broadcast `PATCH /v1/broadcasts/{id}` Updates name, description, scheduledFor, and/or audience before sending starts. At least one field is required. Audience accepts all or an existing segment for the broadcast contact schema; custom filter generation and content authoring are not available here. Concurrent changes or a planning claim can return 409 broadcast_changed; a started send cannot be edited. Supports Idempotency-Key. Cancel/stop/resume/redrive controls are available through app workflows, not this public endpoint. Authentication: Bearer API key required. ### Parameters - `id` (path, string, required): The broadcast id. - `Idempotency-Key` (header, string, optional): Stable key for one logical write, maximum 256 characters. Scoped to calling API key, method, and resolved path, with a canonical request-body fingerprint. Replays recorded status/body with Idempotent-Replay=true. Failed record create/upsert requests retain status and stable error code with a constant message; detailed validation information is only in the first authorized response. Follow the code's retry policy, rather than creating a new key from the HTTP status alone. Changed input returns 422 idempotency_key_reused; active work returns 409 idempotency_conflict; unresolved work returns idempotency_outcome_unknown. Ordinary opaque keys keep their existing 24-hour completed-response retention. For record create/upsert that needs missing AI auto-fill, use rpi1.<13-digit Unix milliseconds>.. The rpi1. prefix is reserved for timed record keys and is checked before cached-response replay. Its retry window ends 24 hours after the issue time rounded up to the next minute. A clock more than five minutes ahead is refused. Expired keys are permanently refused, including after receipt cleanup. An expired or unknown result must be reconciled; do not automatically mint a new key. Unresolved financial/provider work remains retained. Erased record responses return 410 instead of replaying private data. Other idempotent operations keep their own key rules. ### Request body Required. At least one field must be provided. Only `name`/`description`/ `audience`/`scheduledFor` are updatable; content is not editable through this endpoint. Rejected once sending has started. - `name` (string | null, optional): - `description` (string | null, optional): - `audience` (object, optional): The audience to point a broadcast at: everyone in its contact schema, or an existing segment. A `custom filter` audience is authored in the maxclicks app, where a human reviews the generated condition before it drives a mass send, and cannot be set here. - `scheduledFor` (string (date-time) | null, optional): ### Request body example ```json { "name": "September customer update" } ``` ### Response example ```json { "data": { "id": "bc_BcwDvBUeSaSDILA5tHgpmU7I", "createdAt": "2025-01-15T10:30:00.000Z", "updatedAt": "2025-01-15T10:30:00.000Z", "name": "string", "description": "string", "contactSchemaId": "sch_BcwDvBUeSaSDILA5tHgpmU7I", "contactSchemaSlug": "string", "schedulingStatus": "none", "scheduledFor": "2025-01-15T10:30:00.000Z", "sendingStartedAt": "2025-01-15T10:30:00.000Z", "sendingFinishedAt": "2025-01-15T10:30:00.000Z", "failureReason": "string", "audience": { "type": "all" }, "hasContent": false }, "warnings": [] } ``` ### Responses - 200: The updated broadcast. - 400: The request was malformed or failed validation. Also covers `malformed_json` (body is not valid JSON) and, on list endpoints, `offset_too_large`. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 404: The referenced resource was not found. - 409: The write conflicts with an existing resource or the resource's current state. - 413: The request body exceeds the 10 MB limit. - 415: The request used an unsupported content type, charset, or content encoding. - 422: The request was well-formed but could not be processed, typically a failed AI-generation call or a readiness gate (which may carry `error.issues`). - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. - 503: The operation outcome could not be confirmed. Preserve Operation-Id and the original key/body; reconcile before a new operation. ## Send a broadcast `POST /v1/broadcasts/{id}/send` Schedules a prepared broadcast immediately or for scheduledFor and returns 202. It does not wait for planning or delivery. Content is authored in the app. A pre-existing send schedule, started send, or active planning claim returns 409 broadcast_already_sending. Readiness issues return 422 broadcast_invalid with issues. Supports Idempotency-Key. Read runs to inspect recipients; HTTP acceptance is not proof every recipient received email. Authentication: Bearer API key required. ### Parameters - `id` (path, string, required): The broadcast id. - `Idempotency-Key` (header, string, optional): Stable key for one logical write, maximum 256 characters. Scoped to calling API key, method, and resolved path, with a canonical request-body fingerprint. Replays recorded status/body with Idempotent-Replay=true. Failed record create/upsert requests retain status and stable error code with a constant message; detailed validation information is only in the first authorized response. Follow the code's retry policy, rather than creating a new key from the HTTP status alone. Changed input returns 422 idempotency_key_reused; active work returns 409 idempotency_conflict; unresolved work returns idempotency_outcome_unknown. Ordinary opaque keys keep their existing 24-hour completed-response retention. For record create/upsert that needs missing AI auto-fill, use rpi1.<13-digit Unix milliseconds>.. The rpi1. prefix is reserved for timed record keys and is checked before cached-response replay. Its retry window ends 24 hours after the issue time rounded up to the next minute. A clock more than five minutes ahead is refused. Expired keys are permanently refused, including after receipt cleanup. An expired or unknown result must be reconciled; do not automatically mint a new key. Unresolved financial/provider work remains retained. Erased record responses return 410 instead of replaying private data. Other idempotent operations keep their own key rules. ### Request body Optional. An empty or omitted body sends now; a `scheduledFor` schedules for later. - `scheduledFor` (string (date-time), optional): ### Request body example ```json {} ``` ### Response example ```json { "data": { "id": "bc_BcwDvBUeSaSDILA5tHgpmU7I", "createdAt": "2025-01-15T10:30:00.000Z", "updatedAt": "2025-01-15T10:30:00.000Z", "name": "string", "description": "string", "contactSchemaId": "sch_BcwDvBUeSaSDILA5tHgpmU7I", "contactSchemaSlug": "string", "schedulingStatus": "none", "scheduledFor": "2025-01-15T10:30:00.000Z", "sendingStartedAt": "2025-01-15T10:30:00.000Z", "sendingFinishedAt": "2025-01-15T10:30:00.000Z", "failureReason": "string", "audience": { "type": "all" }, "hasContent": false }, "warnings": [] } ``` ### Responses - 202: The broadcast was accepted for sending. - 400: The request was malformed or failed validation. Also covers `malformed_json` (body is not valid JSON) and, on list endpoints, `offset_too_large`. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 404: The referenced resource was not found. - 409: The write conflicts with an existing resource or the resource's current state. - 413: The request body exceeds the 10 MB limit. - 415: The request used an unsupported content type, charset, or content encoding. - 422: The request was well-formed but could not be processed, typically a failed AI-generation call or a readiness gate (which may carry `error.issues`). - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. - 503: The operation outcome could not be confirmed. Preserve Operation-Id and the original key/body; reconcile before a new operation. ## List a broadcast's runs `GET /v1/broadcasts/{id}/runs` One reduced run record per recipient contact. Error: `broadcast_not_found`. Authentication: Bearer API key required. ### Parameters - `id` (path, string, required): The broadcast id. - `limit` (query, integer, optional): Page size. Clamped to 1..200 with a warning if out of range. - `offset` (query, integer, optional): Rows to skip. Negative values clamp to 0 with a warning; above 10000 returns 400 offset_too_large. pagination.hasMore becomes false when the next offset reaches the cap even if totalCount is larger. Records list has no ad-hoc filter; this is not an unlimited export contract. ### Response example ```json { "data": [ { "id": "bcr_BcwDvBUeSaSDILA5tHgpmU7I", "createdAt": "2025-01-15T10:30:00.000Z", "contactId": "con_BcwDvBUeSaSDILA5tHgpmU7I", "contactEmail": "string", "status": "sent", "failureReason": "string", "email": { "id": "bcr_BcwDvBUeSaSDILA5tHgpmU7I", "sentAt": "2025-01-15T10:30:00.000Z", "opensCount": 0, "clicksCount": 0, "bounced": false, "complained": false } } ], "pagination": { "limit": 0, "offset": 0, "totalCount": 0, "hasMore": false }, "warnings": [] } ``` ### Responses - 200: A page of broadcast runs. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 404: The referenced resource was not found. - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. ## Get a broadcast's aggregated email metrics `GET /v1/broadcasts/{id}/metrics` `email` is omitted entirely when the broadcast has no email content yet. Error: `broadcast_not_found`. Authentication: Bearer API key required. ### Parameters - `id` (path, string, required): The broadcast id. ### Response example ```json { "data": { "email": { "sent": 0, "delivered": 0, "bounced": 0, "deliveryDelays": 0, "rejected": 0, "failed": 0, "complained": 0, "unsubscribed": 0, "opens": 0, "uniqueOpens": 0, "clicks": 0, "uniqueClicks": 0 } }, "warnings": [] } ``` ### Responses - 200: Aggregated send/open/click/bounce metrics. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 404: The referenced resource was not found. - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. --- # Domains API reference Source: https://docs.maxclicks.ai/api/domains Read sending domains and their DNS verification status. Domains are added and verified in the app. Base URL: https://api.maxclicks.ai ## List domains `GET /v1/domains` Items omit `records` (unlike `domains.get`); call `domains.get` for DNS records. Authentication: Bearer API key required. ### Parameters - `limit` (query, integer, optional): Page size. Clamped to 1..200 with a warning if out of range. - `offset` (query, integer, optional): Rows to skip. Negative values clamp to 0 with a warning; above 10000 returns 400 offset_too_large. pagination.hasMore becomes false when the next offset reaches the cap even if totalCount is larger. Records list has no ad-hoc filter; this is not an unlimited export contract. ### Response example ```json { "data": [ { "id": "dom_BcwDvBUeSaSDILA5tHgpmU7I", "createdAt": "2025-01-15T10:30:00.000Z", "updatedAt": "2025-01-15T10:30:00.000Z", "name": "string", "customReturnPath": "string", "verificationStarted": false, "verifiedForSending": false, "records": [ { "type": "string", "name": "string", "value": "string", "priority": 0, "status": "not started" } ] } ], "pagination": { "limit": 0, "offset": 0, "totalCount": 0, "hasMore": false }, "warnings": [] } ``` ### Responses - 200: A page of domains. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. ## Get a domain `GET /v1/domains/{id}` Includes live-checked `records`. Error: `domain_not_found`. Authentication: Bearer API key required. ### Parameters - `id` (path, string, required): The domain id. ### Response example ```json { "data": { "id": "dom_BcwDvBUeSaSDILA5tHgpmU7I", "createdAt": "2025-01-15T10:30:00.000Z", "updatedAt": "2025-01-15T10:30:00.000Z", "name": "string", "customReturnPath": "string", "verificationStarted": false, "verifiedForSending": false, "records": [ { "type": "string", "name": "string", "value": "string", "priority": 0, "status": "not started" } ] }, "warnings": [] } ``` ### Responses - 200: The domain. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 404: The referenced resource was not found. - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. --- # Emails API reference Source: https://docs.maxclicks.ai/api/emails Public email recipient actions. Base URL: https://api.maxclicks.ai ## One-click unsubscribe `POST /v1/emails/{emailId}/unsubscribe` Public RFC 8058 one-click unsubscribe target. Unsubscribes the linked contact from a topic or the whole email channel, then redirects (302) to the hosted result page. No rate limit at all on this endpoint. Authentication: no API key required. ### Parameters - `emailId` (path, string, required): The id of the sent email. ### Responses - 302: Redirect to the hosted unsubscribe result page. - 400: The request was malformed or failed validation. Also covers `malformed_json` (body is not valid JSON) and, on list endpoints, `offset_too_large`. - 404: The referenced resource was not found. --- # Errors Source: https://docs.maxclicks.ai/api/errors # Errors Use the HTTP status to decide how to handle a failure, then `error.code` to identify the cause. Correct invalid requests, wait for rate limits, and reconcile uncertain writes before starting another operation. Standard errors use this JSON envelope: ```json { "error": { "type": "invalid_request_error", "code": "schema_not_found", "message": "Schema not found." } } ``` A success body is `{ "data": ... }`, plus a `pagination` block on list endpoints and an optional top-level `warnings` array of strings. An aborting event batch is an exception: HTTP `422` returns `data.results`, `data.summary`, and `data.failedIndex`, so the caller can see rollback results. Inspect that business-result envelope before applying the standard error parser. ## The error object `type` (string) The coarse category, derived from the status class. `invalid_request_error` for every 4xx, `api_error` for every 5xx. Clients should tolerate a missing `type` on an unresolved-operation response. `code` (string | null) The stable, machine-readable cause. Branch on this. It is `null` when the failure has no specific code (most `400` and `403` cases, and most `5xx`). Some server failures carry a code, including `idempotency_outcome_unknown`. `message` (string) A human-readable description. For 4xx it explains the failure. For 5xx it is a generic per-status label and never leaks internal detail. `issues` (string\[]) Present on validation failures that expose details: for example several readiness problems for `broadcast_invalid`, or the missing attribute key for `required_attribute_missing`. ## Status codes Common response statuses are listed below. Successful calls include `200`, `201` when a record, segment, or webhook is created, and `202` on `events.fire` and `broadcasts.send`, so do not test for `200` alone. | Status | `type` | Typical cause | | ---------------------------- | ----------------------- | ----------------------------------------------------------------------- | | `400 Bad Request` | `invalid_request_error` | Input failed validation, `malformed_json`, or `offset_too_large` | | `401 Unauthorized` | `invalid_request_error` | Missing or unknown API key | | `402 Payment Required` | `invalid_request_error` | `insufficient_credits` or `email_limit_reached` | | `403 Forbidden` | `invalid_request_error` | Valid key, but the membership lacks the needed permission | | `404 Not Found` | `invalid_request_error` | Schema, record, or route not found | | `405 Method Not Allowed` | `invalid_request_error` | `events_immutable`: events cannot be edited or deleted | | `409 Conflict` | `invalid_request_error` | Identity collision, duplicate, or in-use resource | | `413 Payload Too Large` | `invalid_request_error` | Request body over 10 MB or normalized event input over 256 KiB | | `415 Unsupported Media Type` | `invalid_request_error` | Unsupported request charset or content encoding | | `422 Unprocessable Entity` | `invalid_request_error` | A validation gate or AI generation step failed | | `429 Too Many Requests` | `invalid_request_error` | Rate-limit bucket exceeded | | `500 Internal Server Error` | `api_error` | Unexpected server-side failure | | `501 Not Implemented` | `api_error` | Server does not support the requested operation | | `502 Bad Gateway` | `api_error` | Upstream failure | | `503 Service Unavailable` | `api_error` or omitted | A service is unavailable or an operation outcome could not be persisted | ## Common codes `code` is the value to branch on. This is a representative set; individual endpoints document their own codes. Any failure without a specific code returns `code: null`, so fall back to the status. | Code | Status | Emitted when | | ----------------------------- | ---------- | -------------------------------------------------------------------------------------- | | `invalid_api_key` | 401 | The API key is missing or unknown | | `insufficient_credits` | 402 | An AI-backed call, or a write into a schema with AI auto-fill, has no credits | | `email_limit_reached` | 402 | A `templates.send` exceeds the email allowance | | `endpoint_not_found` | 404 | The path matched no route | | `schema_not_found` | 404 | The `:schema` segment resolved to no schema, or the wrong schema type for the endpoint | | `record_not_found` | 404 | A contact or object id matched no record in the schema | | `events_immutable` | 405 | You attempted to update or delete an event | | `identifier_conflict` | 409 | A create or update collides with another record's identity | | `suppression_already_exists` | 409 | An identical suppression already exists in your space | | `malformed_json` | 400 | The request body is not valid JSON | | `payload_too_large` | 413 | The body exceeds 10 MB | | `rate_limit_exceeded` | 429 | A request rate bucket was exceeded | | `required_attribute_missing` | 400 | A required writable field has no value or default; `issues` names the key | | `event_id_conflict` | 409 | The same event occurrence identifier received different normalized input | | `event_erased` | 409 | The occurrence was erased and cannot be replayed | | `event_acceptance_unknown` | 409 | An earlier occurrence has no recoverable acceptance receipt | | `event_rate_limited` | 429 | Event admission rate exceeded, separate from API request buckets | | `event_backlog_full` | 429 | Too many pending event preparations | | `event_receipt_limit` | 429 | Receipt capacity is full; waiting alone may not free it | | `idempotency_conflict` | 409 | The same operation is still processing | | `idempotency_outcome_unknown` | 409 or 503 | The original operation needs reconciliation | | `idempotency_key_reused` | 422 | The same key and path received a different body | | `idempotency_admission_busy` | 429 | Operation admission is busy; retry the same key after the indicated delay | `identifier_conflict` reflects the [contact identity cascade](/contacts): `userId`, then `email`, then `phone` for contacts, and `externalId` for objects. Use `records/upsert` to create-or-update by identity instead of failing on a strict create. ## Server errors A `5xx` means the request could not complete normally. Standard error responses use `type: "api_error"` and a generic message. Keep the status, code, operation key, and any returned operation identifier in your logs. ```json { "error": { "type": "api_error", "code": null, "message": "Internal server error." } } ``` A read can usually be retried with bounded exponential backoff and jitter. For a write, the response alone may not establish whether the action took effect. On [supported endpoints](/api/idempotency#supported-endpoints), retain the original `Idempotency-Key` and payload. A recorded error can be replayed, and an unresolved outcome requires investigation. If the server reports `idempotency_outcome_unknown`, inspect the affected resource or run history before starting a new operation. A `503` produced while persisting an outcome can contain `error.operationId` and omit `error.type`; branch on the status and code without assuming `type` is present. ## Rate limits A `429` means admission was refused. Rate and backlog limits can recover after a delay; receipt or identity capacity exhaustion can require operational reconciliation. Rate-limit failures use `rate_limit_exceeded`; an idempotent operation that cannot yet be admitted can use `idempotency_admission_busy`. Respect `Retry-After` before repeating the request. The header can be an HTTP date **or a delay in seconds**. Handle both, and use a fallback if it is absent or invalid: ```javascript function retryAfterMs(value, now = Date.now()) { if (!value) return 1000 const seconds = Number(value) if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000) const date = Date.parse(value) return Number.isFinite(date) ? Math.max(0, date - now) : 1000 } const waitMs = retryAfterMs(response.headers.get('Retry-After')) const delayWithJitter = waitMs + Math.random() * 250 ``` Use that delay in a bounded retry loop. See [rate limits](/api/rate-limit) for request budgets and batching. ## Handling errors Classify the outcome before retrying. These decisions apply whether you call the API directly or through a client library: | Condition | Next action | | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | | `401` | Check the configured key; do not keep retrying an invalid credential. | | `403` | Check the key's space and the owner's permissions. | | Validation, identity, or consent error | Correct the cause. Treat a corrected body as a separate operation if the original key has been claimed. | | `429` | Respect `Retry-After`; preserve the original key and body. Check capacity errors before scheduling more retries. | | `idempotency_conflict` | Wait, then retry the same operation. | | `idempotency_outcome_unknown` | Reconcile the original action before creating another one. | | `5xx` or a timeout on a read | Retry with a bounded backoff. | | `5xx` or a timeout on a write | Follow the endpoint's idempotency or deduplication contract; do not blindly resend. | A successful HTTP status can still contain an operation-level failure. For example, `templates.send` can return `200` with `data.status: "failed"`. Inspect the documented result fields as well as `response.ok`. Template `failed` also covers uncertain provider outcomes and retryable refusals; retain `emailId` and the operation key and reconcile before issuing a new send. --- # Events API reference Source: https://docs.maxclicks.ai/api/events Accept durable business events, inspect readiness, and bind anonymous source identities to contacts. Base URL: https://api.maxclicks.ai ## List events `GET /v1/events` Lists only ready business events for one event schema. Results are ordered by occurredAt descending, then id descending. The time filter is inclusive from and exclusive to; defaults are the seven days before now. A range wider than 90 days is clamped with a warning. Uses cursor pagination: no offset and no totalCount. Pending, failed, and outcome_unknown acceptances are absent; use events.status to inspect them. Invalid times return 400 invalid_time_range; malformed cursors return 400 invalid_cursor. Events are read from the operational event store, not the analytics archive. Authentication: Bearer API key required. ### Parameters - `schema` (query, string, required): The event schema id or slug. - `from` (query, string (date-time), optional): Start of the time range (ISO 8601). Defaults to 7 days before `to`. - `to` (query, string (date-time), optional): End of the time range (ISO 8601). Defaults to now. - `cursor` (query, string, optional): Opaque cursor from a previous page's `pagination.nextCursor`. - `limit` (query, integer, optional): Page size. Defaults to 50, capped at 200. ### Response example ```json { "data": [ { "id": "ev_BcwDvBUeSaSDILA5tHgpmU7I", "schemaId": "sch_BcwDvBUeSaSDILA5tHgpmU7I", "schemaSlug": "string", "eventId": "order_10014", "createdAt": "2025-01-15T10:30:00.000Z", "receivedAt": "2025-01-15T10:30:00.000Z", "occurredAt": "2025-01-15T10:30:00.000Z", "readyAt": "2025-01-15T10:30:00.000Z", "readinessState": "pending", "definitionRevision": "string", "contactId": "con_BcwDvBUeSaSDILA5tHgpmU7I", "objectId": "ev_BcwDvBUeSaSDILA5tHgpmU7I" } ], "pagination": { "limit": 0, "nextCursor": "string", "hasMore": false }, "warnings": [] } ``` ### Responses - 200: A cursor page of events. - 400: The request was malformed or failed validation. Also covers `malformed_json` (body is not valid JSON) and, on list endpoints, `offset_too_large`. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 404: The referenced resource was not found. - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. ## Fire an event `POST /v1/events/{schema}` Durably accepts one business event and returns 202 with its immutable acceptance receipt. Missing AI auto-fill fields are prepared asynchronously; readinessAtAcceptance is pending or ready. An accepted event is visible to event lists and consumers only once ready. Supply a stable eventId and retry the same normalized payload and occurredAt to recover the original receipt with isDuplicate=true. Changed input under the same eventId returns 409 event_id_conflict; erased occurrences return 409 event_erased. Idempotency-Key is not used. The normalized event input is limited to 256 KiB. occurredAt must be within the previous 366 days or five minutes into the future. contactId and objectId are optional, mutually exclusive explicit subjects in this space. Event admission has separate rate, backlog, and receipt-storage limits in addition to the API write bucket. Authentication: Bearer API key required. ### Parameters - `schema` (path, string, required): The event schema id or slug. ### Request body Required. Flat event acceptance input. Custom writable attribute keys are top-level and normalized against the event schema. Any id is ignored on fire. Reserved readiness fields are not caller-controlled. Supply eventId for retry deduplication. The normalized input has a 256 KiB ceiling. - `eventId` (string | null, optional): Caller-owned occurrence identifier, scoped to the space and event schema. Reuse with identical input and occurredAt for retries. If omitted or null, the server generates a UUID; save the returned eventId. A changed payload under the same value returns event_id_conflict. - `occurredAt` (string (date-time), optional): When the event occurred. Defaults to acceptance time; explicit values must be no more than 366 days old or five minutes in the future. Preserve the same explicit value on retries. - `contactId` (string | null, optional): Explicit contact subject id in this space. Mutually exclusive with objectId. - `objectId` (string | null, optional): Explicit object subject id in this space. Mutually exclusive with contactId. ### Request body example ```json { "eventId": "order_1042_paid", "amount": 42 } ``` ### Response example ```json { "data": { "accepted": 1, "id": "ev_BcwDvBUeSaSDILA5tHgpmU7I", "eventId": "order_1042_paid", "isDuplicate": false, "readinessAtAcceptance": "pending" }, "warnings": [] } ``` ### Responses - 202: Durable event acceptance receipt; inspect readiness separately. - 400: The request was malformed or failed validation. Also covers `malformed_json` (body is not valid JSON) and, on list endpoints, `offset_too_large`. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 404: The referenced resource was not found. - 409: The write conflicts with an existing resource or the resource's current state. - 413: The request body exceeds the 10 MB limit. - 415: The request used an unsupported content type, charset, or content encoding. - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. ## Fire a batch of events `POST /v1/events/{schema}/batch` Accepts 1–500 events of one schema. With onError=continue (default), each event commits independently; inspect every result, including failures, even on HTTP 200. Accepted items include their immutable receipt. Admission failures include retryAfterMilliseconds. With onError=abort, a validation or admission failure rolls back new acceptances in that transaction, marks earlier accepted results rolled_back, and returns HTTP 422 with data.results, data.summary, and data.failedIndex. Later items are not attempted. Existing receipts encountered as duplicates remain existing occurrences. A 422 abort response is a business-result data envelope, not the standard error envelope. Each item uses eventId deduplication; Idempotency-Key is not used. Individual events retain the 256 KiB and occurredAt limits, and the whole HTTP body remains limited to 10 MB. Authentication: Bearer API key required. ### Parameters - `schema` (path, string, required): A schema id or slug. ### Request body Required. - `events` (object[], required): - `onError` (string, optional): Allowed values: "continue", "abort". Default: "continue". ### Request body example ```json { "events": [ { "eventId": "order_1042_paid", "amount": 42 } ], "onError": "continue" } ``` ### Response example ```json { "data": { "results": [ { "index": 0, "status": "accepted", "id": "ev_BcwDvBUeSaSDILA5tHgpmU7I", "eventId": "order_1042_paid", "isDuplicate": false, "readinessAtAcceptance": "pending" } ], "summary": { "accepted": 1, "failed": 0 } }, "warnings": [] } ``` ### Responses - 200: Per-item results and a summary. - 400: The request was malformed or failed validation. Also covers `malformed_json` (body is not valid JSON) and, on list endpoints, `offset_too_large`. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 404: The referenced resource was not found. - 413: The request body exceeds the 10 MB limit. - 415: The request used an unsupported content type, charset, or content encoding. - 422: Batch aborted; earlier new acceptances were rolled back. Inspect data.results and data.failedIndex. - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. ## Get event readiness `GET /v1/events/{schema}/{id}/status` Read the current readiness of an accepted event by its maxclicks event id, including pending, ready, failed, and outcome_unknown states. This is distinct from the immutable readinessAtAcceptance returned when firing or retrying the event. data contains raw acceptance values until preparation is ready. A missing event or event schema returns 404. Acceptance and readiness do not prove that a downstream workflow, webhook, or email completed. Authentication: Bearer API key required. ### Parameters - `schema` (path, string, required): Event schema id or slug. - `id` (path, string, required): The maxclicks event id from the acceptance receipt, not your eventId. ### Response example ```json { "data": { "id": "ev_BcwDvBUeSaSDILA5tHgpmU7I", "eventId": "order_1042_paid", "receivedAt": "2026-09-14T10:30:00.000Z", "occurredAt": "2026-09-14T10:30:00.000Z", "readyAt": null, "readinessState": "pending", "definitionRevision": "f2d9685136d4746456815a2b8a6d7a3f9eba98dfc4d6129497778d5037410d7a", "contactId": null, "objectId": null, "data": { "amount": 42 } }, "warnings": [] } ``` ### Responses - 200: Current event readiness and accepted/prepared data. - 400: The request was malformed or failed validation. Also covers `malformed_json` (body is not valid JSON) and, on list endpoints, `offset_too_large`. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 404: The referenced resource was not found. - 409: The write conflicts with an existing resource or the resource's current state. - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. ## Bind an anonymous event identity `POST /v1/event-identities` Binds or unbinds an anonymous browser identity to a contact using a server-held API key with write permission. An enabled contact track definition must match sourceId and subjectSchemaId in the space. Send contactId=null to remove the binding. All six body fields are required and unknown fields are rejected. Use a UUID operationId for replay of the same input; a changed body under that operationId returns 409. expectedAliasRevision is the last revision of this specific alias as a decimal string, or "0" for a new alias. Keep the returned identityRevision for the next change to that alias. Revision conflicts and erased aliases return 409; an unavailable source returns 403. Capacity is bounded at 100,000 aliases and 1,000,000 changes per space; exhausted capacity returns 429 without promising automatic recovery. Idempotency-Key is not used. Authentication: Bearer API key required. ### Request body Required. - `sourceId` (string, required): Source id matching an enabled contact track definition. - `subjectSchemaId` (string, required): Contact schema id (not a slug) used by the track. - `anonymousId` (string, required): Browser identity in the configured source. - `contactId` (string | null, required): Contact in the subject schema, or null to unbind. - `operationId` (string (uuid), required): Stable UUID for this identity change; reuse the same body on retries. - `expectedAliasRevision` (string, required): Last known revision for this alias. Use "0" only for a new alias. ### Request body example ```json { "sourceId": "source_website", "subjectSchemaId": "sch_BcwDvBUeSaSDILA5tHgpmU7I", "anonymousId": "visitor_1042", "contactId": "con_BcwDvBUeSaSDILA5tHgpmU7I", "operationId": "e125aecb-8b4e-4c7e-8f08-8f768bced7ab", "expectedAliasRevision": "0" } ``` ### Response example ```json { "data": { "identityRevision": "string", "affectedSubjectIds": [ "ev_BcwDvBUeSaSDILA5tHgpmU7I" ], "isDuplicate": false }, "warnings": [] } ``` ### Responses - 200: The committed identity revision. - 400: The request was malformed or failed validation. Also covers `malformed_json` (body is not valid JSON) and, on list endpoints, `offset_too_large`. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 404: The referenced resource was not found. - 409: The write conflicts with an existing resource or the resource's current state. - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. --- # Forms API reference Source: https://docs.maxclicks.ai/api/forms Public form submission and double opt-in confirmation. Base URL: https://api.maxclicks.ai ## Submit a form `POST /v1/forms/{formId}/submit` Public, unauthenticated form submission (the tenant is derived from the form, not an API key). Validates required fields, CAPTCHA, and consent, then upserts the contact or (for double opt-in, when the visitor opted in to email) stores a pending record and emails a confirmation link. A declined-consent submission always goes through the direct path even on a double opt-in form. Rate limits: per IP 10/60s and per form 120/60s (both fail open if Redis is down), plus a separate per-recipient/per-form cap on confirmation emails. Error: `rate_limit_exceeded`. A direct submission that needs missing AI auto-fill values requires a timed submissionId. Retain it across network and CAPTCHA retries; do not generate a new ID for an unanswered request. The ID uses the same format and retry window as timed record Idempotency-Key values. Confirmation has its own persisted server identity and token expiry. Authentication: no API key required. ### Parameters - `formId` (path, string, required): The form id. ### Request body Required. - `submissionId` (string, optional): Stable ID for one input, required when direct contact creation needs AI auto-fill. Format rpi1... Keep the original ID with the original input for retries. The recovery window is 24 hours rounded up to the next minute. - `contact` (object, required): Flat contact input for the form's contact schema. Custom attribute keys are passed at the top level. - `emailMarketingConsent` (boolean | null, optional): - `captchaToken` (string | null, optional): ### Request body example ```json { "contact": { "email": "sarah@example.com", "firstName": "Sarah" }, "emailMarketingConsent": false } ``` ### Response example ```json { "data": { "contactId": "con_BcwDvBUeSaSDILA5tHgpmU7I" }, "warnings": [] } ``` ### Responses - 200: The submission was accepted. - 400: The request was malformed or failed validation. Also covers `malformed_json` (body is not valid JSON) and, on list endpoints, `offset_too_large`. - 404: The referenced resource was not found. - 410: The record operation expired or its stored response was erased. Reconcile the original operation before starting a new one. - 413: The request body exceeds the 10 MB limit. - 415: The request used an unsupported content type, charset, or content encoding. - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. ## Confirm a double opt-in `GET /v1/forms/{formId}/confirm/{token}` Public browser navigation target for the double opt-in email link. Redeems the token, upserts the confirmed contact, and always redirects (302) to the hosted result page. Every failure path (missing/expired token, unpublished form) also redirects to a "failed" landing page rather than returning a JSON error. Same rate limits as `forms.submit`. Authentication: no API key required. ### Parameters - `formId` (path, string, required): The form id. - `token` (path, string, required): The single-use double opt-in token from the confirmation email. ### Responses - 302: Redirect to the hosted confirmation result page. --- # Idempotency Source: https://docs.maxclicks.ai/api/idempotency # Idempotency A dropped connection should not become a second email or workflow run. On supported writes, send an `Idempotency-Key` so maxclicks can identify a retry of the same operation. Ordinary cached writes replay a recorded response; template sends and record deletion have their own durable operation rules below. Keep the key, payload, and returned result together in your integration. If the outcome is uncertain, reuse the original key while you investigate; a new key represents a new operation. ## The header Choose a unique key for each logical operation, such as a UUID or a source event ID. Keys can be up to **256 characters**. Send the same value and body for every retry of that operation. ```bash curl -X POST https://api.maxclicks.ai/v1/schemas/customers/records/upsert \ -H "Authorization: Bearer $MAXCLICKS_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: customer-sync-1042-revision-7" \ -d '{ "userId": "1042", "email": "sarah@example.com" }' ``` Replace `customers` with your contact schema's slug. Include a source revision in a sync key when the same customer's data can change later. ## Lifecycle | Result | What your integration should do | | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | First request | Save the operation's key and body. The server claims the operation before executing it. | | Completed request | Save the result and any `Operation-Id` response header. A repeat can return the recorded status and body with `Idempotent-Replay: true`. | | `409 idempotency_conflict` | The original is still processing. Back off and retry with the same key and body. | | `409 idempotency_outcome_unknown` | The original outcome is unresolved. Check the affected record, email, broadcast, or workflow before taking another action. | | `422 idempotency_key_reused` | The key was used with a different body. Recover the original payload; use a new key only for a separate operation. | | `429 idempotency_admission_busy` | Wait for `Retry-After`, then retry the same key and body. | | `503 idempotency_outcome_unknown` | The server could not confirm persistence of the outcome. Retain any `operationId` and reconcile; do not force another execution with a new key. | ### Recorded errors and retention Responses produced after an operation is claimed can be recorded, including `4xx` and `5xx` responses. Repeating the request can replay that error; it does not guarantee another execution. Authentication, request parsing, and other checks that run before the claim can fail without creating an idempotency record. Completed ordinary cached operations have a nominal 24-hour retention period. After cache expiry, an ordinary opaque key can execute again. Unresolved operations are retained for reconciliation; elapsed time alone does not make a retry safe. Timed record writes, template sends and deletion attempts have different rules. ## Record writes with AI auto-fill When record create/upsert needs missing AI auto-fill, use a timed key in the form `rpi1.<13-digit Unix milliseconds>.`. Create it once for the operation and persist it with the original input. The `rpi1.` prefix is reserved; an invalid shape is refused. Its retry window ends 24 hours after the issue time rounded up to the next minute. A clock more than five minutes ahead is refused. After expiry, the key is permanently refused with `410 record_operation_expired`, including after response cleanup. Do not make a fresh key automatically to bypass expiry: inspect the record and original operation first. A retry of an erased record response returns `410` instead of private data. A recorded record-write error can retain a stable code with a constant message, so save detailed validation information from the first response. See [record writes](/api/records) and [errors](/api/errors). ## Template sends `templates.send` requires a non-blank key. It retains the message and recipient from the original operation instead of using generic cache expiry or replay headers. A temporary refusal returns `429 template_send_retryable` with `Retry-After`; wait and retry the same key, template and input. An uncertain outcome returns `409 template_send_outcome_unknown`; reconcile it before taking another action. A confirmed terminal failure remains failed on retry. See the [template-send contract](/api/templates#templates.send) for conflicts and outcome checks. A new key requests another send. ## Record deletion attempts `records.delete` requires an explicit attempt key of 1–128 characters. Start with a letter or digit, then use letters, digits, underscores, periods, colons or hyphens. Keep that key to recover a lost response and save the returned `operationId`. The key identifies the deletion attempt; the endpoint returns current operation status rather than a cached response. Poll [deletion status](/api/records#records.getDeletion). After deletion completes, the DELETE route can return `404` because the record is gone, so use the saved operation ID to inspect completion. The original requester with current write permission can [cancel before removal begins](/api/records#records.cancelDeletion). Cancellation waits for intake retirement; `202` is not a completed cancellation. Poll until `cancelled`. A later intentional deletion after cancellation needs a new attempt key; repeating the cancelled attempt does not request another deletion. Do not assume every `5xx` is safe to resend as a new operation. A write may have taken effect before the response was lost. When the API reports an unresolved outcome, inspect the resource or its run history before deciding what to do next. ## What a key is scoped to The server scopes a key to the calling API key, HTTP method, and resolved path. It also compares a canonical hash of the request body. The same key sent to two different template IDs represents two operations; the same key and path with a changed body returns `422 idempotency_key_reused`. Reordering JSON object keys does not change the body identity. Changing values does. Reuse the same calling API key for retries, including while rotating credentials. ## Supported endpoints | Operation | Method and path | | -------------------------- | --------------------------------------------- | | `records.create` | `POST /v1/schemas/{schema}/records` | | `records.upsert` | `POST /v1/schemas/{schema}/records/upsert` | | `suppressions.batchCreate` | `POST /v1/contacts/suppressions/batch` | | `suppressions.batchDelete` | `POST /v1/contacts/suppressions/batch-remove` | | `suppressions.delete` | `DELETE /v1/contacts/suppressions/{id}` | | `segments.create` | `POST /v1/segments` | | `segments.delete` | `DELETE /v1/segments/{id}` | | `webhooks.create` | `POST /v1/webhooks` | | `webhooks.update` | `PATCH /v1/webhooks/{id}` | | `webhooks.delete` | `DELETE /v1/webhooks/{id}` | | `webhooks.rotateSecret` | `POST /v1/webhooks/{id}/rotate-secret` | | `workflows.trigger` | `POST /v1/workflows/{id}` | | `workflows.pause` | `POST /v1/workflows/{id}/pause` | | `workflows.unpause` | `POST /v1/workflows/{id}/unpause` | | `broadcasts.send` | `POST /v1/broadcasts/{id}/send` | | `broadcasts.update` | `PATCH /v1/broadcasts/{id}` | | `templates.send` | `POST /v1/templates/{templateId}/send` | | `records.delete` | `DELETE /v1/schemas/{schema}/records/{id}` | For [workflow triggers](/api/workflows#workflows.trigger), the path uses the incoming-webhook step's reference ID. Copy it from the workflow; it is not the workflow's own ID. Webhook creation and secret rotation replay their original secret-bearing response when retried with the same key. Store that result securely. Normal webhook GET/list responses omit the signing secret. A new key on rotation requests another rotation. The [SDK guides](/sdks) show how to pass an idempotency key in each language. For direct HTTP requests, set the `Idempotency-Key` header; adding a field to the JSON body does not set an HTTP header. ## Other retry mechanisms The header does not add protection to an endpoint outside this table. Record PATCH and single suppression creation do not use this mechanism. Template sends and record deletion use the distinct required identities explained above. * Business event creation uses your `eventId`, scoped to the space and event schema. Preserve the same payload and explicit `occurredAt`. Changed input returns `event_id_conflict`; a duplicate returns its original acceptance receipt. Poll readiness separately. * Event identity changes use the required UUID `operationId` plus `expectedAliasRevision`. Preserve the input when retrying and save the returned revision for that alias. * Cancellation of a record deletion uses the existing `operationId`, with no additional idempotency key. Inspect its state until `cancelled`; cancellation cannot restore removed records. ## Related * [Handle errors](/api/errors) * [Respect rate limits](/api/rate-limit) * [Send your first template](/quickstart#step-5-send-it) --- # Meta API reference Source: https://docs.maxclicks.ai/api/meta Identify the calling API key. Base URL: https://api.maxclicks.ai ## Identify the calling API key `GET /v1/me` Returns the key metadata, its owning user, the bound space (if any), and the caller's role. Works for space-unbound keys, in which case `space` and `role` are null. For an OAuth access token, `oauth` names the resource it was issued for, its granted scopes and its expiry. Authentication: Bearer API key required. ### Response example ```json { "data": { "apiKey": { "id": "max_BcwDvBUeSaSDILA5tHgpmU7I", "name": "string", "keyDisplay": "string", "createdAt": "2025-01-15T10:30:00.000Z" }, "user": { "id": "usr_BcwDvBUeSaSDILA5tHgpmU7I", "email": "contact@example.com", "name": "string" }, "space": { "id": "spc_BcwDvBUeSaSDILA5tHgpmU7I", "slug": "string", "name": "string" }, "role": { "name": "admin", "label": "string", "description": "string" }, "oauth": { "clientId": "string", "resource": "https://example.com", "scopes": [ "read" ], "expiresAt": "2025-01-15T10:30:00.000Z" } }, "warnings": [] } ``` ### Responses - 200: The identity of the calling key. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. --- # Pagination Source: https://docs.maxclicks.ai/api/pagination # Pagination Read customer records, campaign runs, and other lists one page at a time. Each response returns results in `data` and the next-step information in `pagination`. Most lists use offset pagination with `limit` and `offset`. The events list (`GET /v1/events`) is the exception: it uses cursor pagination. ## Offset pagination Pass `limit` and `offset` as query parameters. The response wraps the page in `data` and reports the window in `pagination`. ### Parameters `limit` (integer) Page size. Default `50`. Clamped to the range `1..200` with a warning if out of range. `offset` (integer) Rows to skip. Default `0`. Values below `0` are clamped to `0` with a warning. A value above `10000` is rejected with `400 offset_too_large`, not clamped. ### Response `data` (array) The page of results. `pagination.limit` (integer) The applied page size. `pagination.offset` (integer) The applied offset. `pagination.totalCount` (integer) Total rows matching the query, across all pages. `pagination.hasMore` (boolean) `true` when more rows exist and the next offset is below `10000`. A false value can mean the traversal cap was reached, even when `totalCount` is larger. ```bash {{ title: 'cURL' }} curl "https://api.maxclicks.ai/v1/schemas/customers/records?limit=50&offset=100" \ -H "Authorization: Bearer $MAXCLICKS_API_KEY" ``` ```json {{ title: 'Response' }} { "data": [{ "id": "con_BcwDvBUeSaSDILA5tHgpmU7I" }], "pagination": { "limit": 50, "offset": 100, "totalCount": 4213, "hasMore": true } } ``` Replace `customers` with your schema slug. Page forward by advancing `offset` by the **returned** `pagination.limit` while `pagination.hasMore` is `true`. `offset` cannot exceed `10000`, and normal traversal stops before the next offset reaches that cap. This is not an unlimited export API. The records list has no ad-hoc filter or time-range parameter; use a suitable segment where supported, or arrange a full export in the app instead of inventing query fields. ### Endpoints Offset pagination applies to every list except `GET /v1/events`: `schemas.list`, `attributes.list`, `records.list`, `records.auditTrail`, `suppressions.list`, `domains.list`, `senders.list`, `topics.list`, `segments.list`, `segments.listContacts`, `broadcasts.list`, `broadcasts.listRuns`, `templates.list`, `webhooks.list`, `workflows.list`, and `workflows.listRuns`. `segments.listContacts` is the one endpoint with a lower ceiling: it caps the page at 100 rows whatever `limit` you send, and it emits no warning when it truncates. Ask for 200 there and you get 100 rows back with `pagination.limit` reporting `100`. Use the returned page size when advancing the offset. ## Cursor pagination `GET /v1/events` returns only **ready** business events, ordered by `occurredAt` descending and then `id` descending. It has no `offset` and no `totalCount`. Instead, each page returns an opaque `nextCursor` that you pass back as `cursor` to fetch the next page. ### Parameters `limit` (integer) Page size. Default `50`, capped at `200`. `cursor` (string) Opaque cursor from the previous page's `pagination.nextCursor`. Omit for the first page. An invalid value returns `400 invalid_cursor`. ### Response `data` (array) The page of events. `pagination.limit` (integer) The applied page size. `pagination.nextCursor` (string | null) Cursor for the next page, or `null` when no more rows exist. `pagination.hasMore` (boolean) `true` when another cursor page is available. ```bash {{ title: 'First page' }} curl "https://api.maxclicks.ai/v1/events?schema=purchase-completed&limit=50" \ -H "Authorization: Bearer $MAXCLICKS_API_KEY" ``` ```json {{ title: 'Response' }} { "data": [{ "id": "ev_BcwDvBUeSaSDILA5tHgpmU7I", "eventId": "..." }], "pagination": { "limit": 50, "nextCursor": "eyJ0IjoxNz...", "hasMore": true } } ``` ```bash {{ title: 'Next page' }} curl "https://api.maxclicks.ai/v1/events?schema=purchase-completed&cursor=eyJ0IjoxNz..." \ -H "Authorization: Bearer $MAXCLICKS_API_KEY" ``` Loop until `pagination.hasMore` is `false` (or `nextCursor` is `null`), passing the returned `nextCursor` as `cursor` each time. Set explicit `from` and `to` timestamps on your first request, and keep `schema`, `from`, `to`, and `limit` fixed across pages. This avoids a moving default time window during a long export. The events list reads the operational event store. `from` is inclusive and `to` is exclusive; defaults are the last 7 days. A range wider than 90 days is clamped with a warning. Pending, failed, and unresolved events are absent; use [event readiness](/api/events#events.status) to inspect an acceptance. ## Workflow step history A workflow run embeds at most 100 step-history records. Its initial window is the latest 100, ordered by `index` ascending within that window. To inspect earlier execution, call `GET /v1/workflows/{id}/runs/{runId}?history_before=...` with the returned `nextHistoryBeforeIndex`. Continue until that field is `null`. This cursor is separate from the offset pagination of the run list. `stepCount` is the execution ordinal, not the number of entries in one window. ## Related * [Events](/events) * [Errors](/api/errors) * [Rate limits](/api/rate-limit) --- # Rate limits Source: https://docs.maxclicks.ai/api/rate-limit # Rate limits Pace requests so customer updates, campaigns, and journeys keep moving predictably. The API applies three request budgets per API key. Exceeding a budget returns `429` with a `Retry-After` header. Services using the same key share its budgets. Coordinate background sync jobs and interactive traffic accordingly. ## Buckets | Bucket | Limit | Window | Applies to | | ------- | ------------ | ---------- | ------------------------------------------------------------ | | `read` | 100 requests | 1 second | Every authenticated `GET` (except the form endpoints below). | | `write` | 25 requests | 1 second | Every other `POST`, `PATCH`, `DELETE`. | | `ai` | 10 requests | 60 seconds | LLM-backed endpoints. | Each bucket is an independent counter. Listing records, fetching a record, and `GET /v1/me` draw from `read`. Creating, upserting, patching, deleting records, firing events, sending a template, and triggering a workflow draw from `write`. ## AI bucket These endpoints generate an expression with an LLM and draw from the `ai` bucket: | Endpoint | Condition | | ---------------------------------------------- | -------------------------------------------------- | | `POST /v1/segments` | Always. | | `POST /v1/webhooks`, `PATCH /v1/webhooks/{id}` | Only when the body sets a custom filter condition. | When the condition is not met, the request draws from `write` instead. The AI bucket does not describe every paid operation. Record writes can require AI auto-fill credits, and event preparation can wait for AI capacity or credits after acceptance. Those requests still use the write bucket. Content, evaluated attributes, and broadcast audiences are authored in the app. An `ai` request that has no credits left returns `402 insufficient_credits`, which is separate from the rate limit. ## Exceeding a limit Over the limit, the API returns `429` with the standard error envelope. The `code` is `rate_limit_exceeded`: ```json { "error": { "type": "invalid_request_error", "code": "rate_limit_exceeded", "message": "Rate limit exceeded, please try again later." } } ``` See [Errors](/api/errors) for the full envelope and the code list. ### Retry-After Rate-limit responses use an HTTP date in UTC to indicate when the window resets. Other `429` responses, such as `idempotency_admission_busy`, can use delay seconds. Support both formats; see the [parser example](/api/errors#rate-limits). Successful responses do not include a remaining-request counter. ```http Retry-After: Wed, 21 Oct 2026 07:28:00 GMT ``` Wait until that time, then retry. Add jitter to spread retries. ## Event admission Business events also pass shared admission limits per fleet, space, and schema. These apply to every new occurrence, including each item in a batch; changing API keys does not bypass them. They are operational limits, separate from your subscription and the HTTP request buckets. | Scope | Sustained rate | Burst | Pending preparation | Retained receipts | | ------ | ----------------- | ----- | ------------------- | ----------------- | | Fleet | 100 events/second | 200 | 10,000 | 1,000,000 | | Space | 10 events/second | 30 | 1,000 | 50,000 | | Schema | 5 events/second | 15 | 500 | 20,000 | `event_rate_limited` reports a computed delay. `event_backlog_full` asks for a 30-second delay. `event_receipt_limit` reports a one-hour delay, but capacity must actually become available before new occurrences can enter; do not repeatedly replace IDs or keys. Single-event failures use `Retry-After`; per-item batch failures include `retryAfterMilliseconds`. A duplicate with matching input recovers its existing receipt without admitting a new occurrence. Event identity changes have separate space limits of 100,000 aliases and 1,000,000 changes. Capacity failures return `429` without a guaranteed retry header. Preserve alias revisions and reconcile capacity before generating more changes. ## Implementation The rate-limit window is fixed. Avoid using the window boundary as a target for a burst: pace requests throughout it, keep concurrent workers within a shared budget, and add jitter when several workers retry. ## Form endpoints The unauthenticated form endpoints (`POST /v1/forms/{id}/submit` and the double opt-in link `GET /v1/forms/{id}/confirm/{token}`) do not use the read/write/ai buckets. They have dedicated limiters: | Limiter | Limit | Window | Keyed on | | -------- | --------------- | ---------- | ---------- | | Per IP | 10 submissions | 60 seconds | Client IP. | | Per form | 120 submissions | 60 seconds | Form ID. | Double opt-in confirmation emails are throttled separately: 2 per recipient per form per hour, and 200 per form per hour. When either trips, the submit endpoint returns `429` with code `rate_limit_exceeded`. Keep these limits in mind when building a custom form experience. `POST /v1/emails/{emailId}/unsubscribe`, the RFC 8058 one-click unsubscribe target, has no rate limiter. ## Handling limits * Classify `429`: wait for rate/backlog limits; resolve exhausted receipt or identity capacity before resuming new writes. * Spread bursty writes over time. The `write` bucket is tighter than `read`. * Batch where the API supports it. Fire many events in one call with `POST /v1/events/{schema}/batch` to reduce HTTP overhead. Each event still consumes event-admission capacity, and each result must be checked. * Records have no batch write. Loop `records.upsert` one record at a time and pace the loop under the `write` bucket. Keep a checkpoint so a partial sync can resume from its last confirmed result. --- # Records API reference Source: https://docs.maxclicks.ai/api/records Keep stored contact and object data current, inspect consent history, and track deletion to completion. Base URL: https://api.maxclicks.ai ## List records `GET /v1/schemas/{schema}/records` Lists stored contact or object records, ordered by createdAt ascending then id ascending. Custom values are flattened at the top level alongside platform fields. Evaluated and aggregate attributes are not expanded. This endpoint accepts limit/offset only; it has no ad-hoc search, time filter, include, or trait-expansion query. Traversal is capped; pagination.hasMore can be false when totalCount exceeds the reachable window. Authentication: Bearer API key required. ### Parameters - `schema` (path, string, required): A schema id or slug. - `limit` (query, integer, optional): Page size. Clamped to 1..200 with a warning if out of range. - `offset` (query, integer, optional): Rows to skip. Negative values clamp to 0 with a warning; above 10000 returns 400 offset_too_large. pagination.hasMore becomes false when the next offset reaches the cap even if totalCount is larger. Records list has no ad-hoc filter; this is not an unlimited export contract. ### Response example ```json { "data": [ { "id": "con_BcwDvBUeSaSDILA5tHgpmU7I", "schemaId": "sch_BcwDvBUeSaSDILA5tHgpmU7I", "schemaSlug": "string", "source": "import", "fullName": "string", "firstName": "string", "lastName": "string", "avatarUrl": "string", "userId": "user_8412", "email": "contact@example.com", "phone": "string", "subscriptions": [ "email" ], "topicIds": [ "ct_BcwDvBUeSaSDILA5tHgpmU7I" ], "notes": "string", "tags": [ "string" ], "emailStatistics": { "sent": 0, "delivered": 0, "bounced": 0, "complained": 0, "unsubscribed": 0, "uniqueOpens": 0, "uniqueClicks": 0 }, "createdAt": "2025-01-15T10:30:00.000Z", "updatedAt": "2025-01-15T10:30:00.000Z" } ], "pagination": { "limit": 0, "offset": 0, "totalCount": 0, "hasMore": false }, "warnings": [] } ``` ### Responses - 200: A page of records. - 400: The request was malformed or failed validation. Also covers `malformed_json` (body is not valid JSON) and, on list endpoints, `offset_too_large`. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 404: The referenced resource was not found. - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. ## Create a record `POST /v1/schemas/{schema}/records` Strictly creates one contact or object record (never updates an existing one). Any `id` in the body is ignored. `source` is forced to `api`. Missing AI auto-fill values require a timed `Idempotency-Key` in the format `rpi1.<13-digit Unix milliseconds>.`. Generate it once and retain it with the input. A completed paid preparation is reused; only a new model call needs new credit admission. Supports `Idempotency-Key`. Errors: `schema_not_found`, `identifier_conflict` (409), `insufficient_credits` (402). Authentication: Bearer API key required. ### Parameters - `schema` (path, string, required): A schema id or slug. - `Idempotency-Key` (header, string, optional): Stable key for one logical write, maximum 256 characters. Scoped to calling API key, method, and resolved path, with a canonical request-body fingerprint. Replays recorded status/body with Idempotent-Replay=true. Failed record create/upsert requests retain status and stable error code with a constant message; detailed validation information is only in the first authorized response. Follow the code's retry policy, rather than creating a new key from the HTTP status alone. Changed input returns 422 idempotency_key_reused; active work returns 409 idempotency_conflict; unresolved work returns idempotency_outcome_unknown. Ordinary opaque keys keep their existing 24-hour completed-response retention. For record create/upsert that needs missing AI auto-fill, use rpi1.<13-digit Unix milliseconds>.. The rpi1. prefix is reserved for timed record keys and is checked before cached-response replay. Its retry window ends 24 hours after the issue time rounded up to the next minute. A clock more than five minutes ahead is refused. Expired keys are permanently refused, including after receipt cleanup. An expired or unknown result must be reconciled; do not automatically mint a new key. Unresolved financial/provider work remains retained. Erased record responses return 410 instead of replaying private data. Other idempotent operations keep their own key rules. ### Request body Required. Input for creating, upserting, or updating a record. All fields are optional. For a contact schema the contact fields apply; for an object schema the object fields apply. Custom attribute keys are passed at the top level. - `id` (string, optional): - `fullName` (string | null, optional): - `firstName` (string | null, optional): - `lastName` (string | null, optional): - `avatarUrl` (string | null, optional): - `userId` (string | null, optional): - `email` (string (email) | null, optional): - `phone` (string | null, optional): - `subscriptions` (string[], optional): - `topicIds` (string[], optional): - `externalId` (string | null, optional): - `notes` (string | null, optional): - `tags` (string[], optional): ### Request body example ```json { "email": "sarah@example.com", "firstName": "Sarah" } ``` ### Response example ```json { "data": { "id": "con_BcwDvBUeSaSDILA5tHgpmU7I", "schemaId": "sch_BcwDvBUeSaSDILA5tHgpmU7I", "schemaSlug": "string", "source": "import", "fullName": "string", "firstName": "string", "lastName": "string", "avatarUrl": "string", "userId": "user_8412", "email": "contact@example.com", "phone": "string", "subscriptions": [ "email" ], "topicIds": [ "ct_BcwDvBUeSaSDILA5tHgpmU7I" ], "notes": "string", "tags": [ "string" ], "emailStatistics": { "sent": 0, "delivered": 0, "bounced": 0, "complained": 0, "unsubscribed": 0, "uniqueOpens": 0, "uniqueClicks": 0 }, "createdAt": "2025-01-15T10:30:00.000Z", "updatedAt": "2025-01-15T10:30:00.000Z" }, "warnings": [] } ``` ### Responses - 201: The created record. - 400: The request was malformed or failed validation. Also covers `malformed_json` (body is not valid JSON) and, on list endpoints, `offset_too_large`. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 402: Insufficient credits (`insufficient_credits`) for an AI-generation call or a schema with AI auto-fill, or the space's email allowance is exhausted (`email_limit_reached`). - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 404: The referenced resource was not found. - 409: The write conflicts with an existing resource or the resource's current state. - 410: The record operation expired or its stored response was erased. Reconcile the original operation before starting a new one. - 413: The request body exceeds the 10 MB limit. - 415: The request used an unsupported content type, charset, or content encoding. - 422: The request was well-formed but could not be processed, typically a failed AI-generation call or a readiness gate (which may carry `error.issues`). - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. - 503: The operation outcome could not be confirmed. Preserve Operation-Id and the original key/body; reconcile before a new operation. ## Upsert a record `POST /v1/schemas/{schema}/records/upsert` Creates or updates a contact or object. An explicit id selects that exact record or returns record_not_found. Otherwise contact matching uses userId first; email can match only a contact without userId; phone can match only a contact without userId or email. Objects match by externalId. Per-field identifier collisions may be skipped and reported in warnings; inspect the returned record. Returns 201 for creation or 200 for update. Any missing AI auto-fill value on a new record requires a timed Idempotency-Key in the format `rpi1.<13-digit Unix milliseconds>.`. Generate it once and retain it with the input. Paid prepared values can complete a retry without new credits; new model calls still require credit admission. Required writable fields without a value/default can fail with required_attribute_missing (400, issues names the key). Authentication: Bearer API key required. ### Parameters - `schema` (path, string, required): A schema id or slug. - `Idempotency-Key` (header, string, optional): Stable key for one logical write, maximum 256 characters. Scoped to calling API key, method, and resolved path, with a canonical request-body fingerprint. Replays recorded status/body with Idempotent-Replay=true. Failed record create/upsert requests retain status and stable error code with a constant message; detailed validation information is only in the first authorized response. Follow the code's retry policy, rather than creating a new key from the HTTP status alone. Changed input returns 422 idempotency_key_reused; active work returns 409 idempotency_conflict; unresolved work returns idempotency_outcome_unknown. Ordinary opaque keys keep their existing 24-hour completed-response retention. For record create/upsert that needs missing AI auto-fill, use rpi1.<13-digit Unix milliseconds>.. The rpi1. prefix is reserved for timed record keys and is checked before cached-response replay. Its retry window ends 24 hours after the issue time rounded up to the next minute. A clock more than five minutes ahead is refused. Expired keys are permanently refused, including after receipt cleanup. An expired or unknown result must be reconciled; do not automatically mint a new key. Unresolved financial/provider work remains retained. Erased record responses return 410 instead of replaying private data. Other idempotent operations keep their own key rules. ### Request body Required. Input for creating, upserting, or updating a record. All fields are optional. For a contact schema the contact fields apply; for an object schema the object fields apply. Custom attribute keys are passed at the top level. - `id` (string, optional): - `fullName` (string | null, optional): - `firstName` (string | null, optional): - `lastName` (string | null, optional): - `avatarUrl` (string | null, optional): - `userId` (string | null, optional): - `email` (string (email) | null, optional): - `phone` (string | null, optional): - `subscriptions` (string[], optional): - `topicIds` (string[], optional): - `externalId` (string | null, optional): - `notes` (string | null, optional): - `tags` (string[], optional): ### Request body example ```json { "userId": "customer_1042", "email": "sarah@example.com", "firstName": "Sarah" } ``` ### Response example ```json { "data": { "id": "con_BcwDvBUeSaSDILA5tHgpmU7I", "schemaId": "sch_BcwDvBUeSaSDILA5tHgpmU7I", "schemaSlug": "string", "source": "import", "fullName": "string", "firstName": "string", "lastName": "string", "avatarUrl": "string", "userId": "user_8412", "email": "contact@example.com", "phone": "string", "subscriptions": [ "email" ], "topicIds": [ "ct_BcwDvBUeSaSDILA5tHgpmU7I" ], "notes": "string", "tags": [ "string" ], "emailStatistics": { "sent": 0, "delivered": 0, "bounced": 0, "complained": 0, "unsubscribed": 0, "uniqueOpens": 0, "uniqueClicks": 0 }, "createdAt": "2025-01-15T10:30:00.000Z", "updatedAt": "2025-01-15T10:30:00.000Z" }, "warnings": [] } ``` ### Responses - 200: The updated record. - 201: The created record. - 400: The request was malformed or failed validation. Also covers `malformed_json` (body is not valid JSON) and, on list endpoints, `offset_too_large`. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 402: Insufficient credits (`insufficient_credits`) for an AI-generation call or a schema with AI auto-fill, or the space's email allowance is exhausted (`email_limit_reached`). - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 404: The referenced resource was not found. - 409: The write conflicts with an existing resource or the resource's current state. - 410: The record operation expired or its stored response was erased. Reconcile the original operation before starting a new one. - 413: The request body exceeds the 10 MB limit. - 415: The request used an unsupported content type, charset, or content encoding. - 422: The request was well-formed but could not be processed, typically a failed AI-generation call or a readiness gate (which may carry `error.issues`). - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. - 503: The operation outcome could not be confirmed. Preserve Operation-Id and the original key/body; reconcile before a new operation. ## Get a record `GET /v1/schemas/{schema}/records/{id}` Reads one contact or object in this schema. Stored custom values and platform fields are flattened; evaluated and aggregate attributes are not expanded. There is no include/expand query. Missing schema or record returns 404. Authentication: Bearer API key required. ### Parameters - `schema` (path, string, required): A schema id or slug. - `id` (path, string, required): The record's platform id. ### Response example ```json { "data": { "id": "con_BcwDvBUeSaSDILA5tHgpmU7I", "schemaId": "sch_BcwDvBUeSaSDILA5tHgpmU7I", "schemaSlug": "string", "source": "import", "fullName": "string", "firstName": "string", "lastName": "string", "avatarUrl": "string", "userId": "user_8412", "email": "contact@example.com", "phone": "string", "subscriptions": [ "email" ], "topicIds": [ "ct_BcwDvBUeSaSDILA5tHgpmU7I" ], "notes": "string", "tags": [ "string" ], "emailStatistics": { "sent": 0, "delivered": 0, "bounced": 0, "complained": 0, "unsubscribed": 0, "uniqueOpens": 0, "uniqueClicks": 0 }, "createdAt": "2025-01-15T10:30:00.000Z", "updatedAt": "2025-01-15T10:30:00.000Z" }, "warnings": [] } ``` ### Responses - 200: The record. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 404: The referenced resource was not found. - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. ## Update a record `PATCH /v1/schemas/{schema}/records/{id}` Partially updates a record. The path id overrides any body id. Explicit null clears a nullable field; omit a field to preserve it. Non-writable evaluated/aggregate keys are not accepted as stored inputs. Normalization and skipped per-field identity updates can produce warnings. The endpoint does not use Idempotency-Key. Errors: schema_not_found, record_not_found, identifier_conflict, or required_attribute_missing (400, issues names the missing key). Authentication: Bearer API key required. ### Parameters - `schema` (path, string, required): A schema id or slug. - `id` (path, string, required): The record's platform id. ### Request body Required. Input for creating, upserting, or updating a record. All fields are optional. For a contact schema the contact fields apply; for an object schema the object fields apply. Custom attribute keys are passed at the top level. - `id` (string, optional): - `fullName` (string | null, optional): - `firstName` (string | null, optional): - `lastName` (string | null, optional): - `avatarUrl` (string | null, optional): - `userId` (string | null, optional): - `email` (string (email) | null, optional): - `phone` (string | null, optional): - `subscriptions` (string[], optional): - `topicIds` (string[], optional): - `externalId` (string | null, optional): - `notes` (string | null, optional): - `tags` (string[], optional): ### Request body example ```json { "firstName": "Sarah" } ``` ### Response example ```json { "data": { "id": "con_BcwDvBUeSaSDILA5tHgpmU7I", "schemaId": "sch_BcwDvBUeSaSDILA5tHgpmU7I", "schemaSlug": "string", "source": "import", "fullName": "string", "firstName": "string", "lastName": "string", "avatarUrl": "string", "userId": "user_8412", "email": "contact@example.com", "phone": "string", "subscriptions": [ "email" ], "topicIds": [ "ct_BcwDvBUeSaSDILA5tHgpmU7I" ], "notes": "string", "tags": [ "string" ], "emailStatistics": { "sent": 0, "delivered": 0, "bounced": 0, "complained": 0, "unsubscribed": 0, "uniqueOpens": 0, "uniqueClicks": 0 }, "createdAt": "2025-01-15T10:30:00.000Z", "updatedAt": "2025-01-15T10:30:00.000Z" }, "warnings": [] } ``` ### Responses - 200: The updated record. - 400: The request was malformed or failed validation. Also covers `malformed_json` (body is not valid JSON) and, on list endpoints, `offset_too_large`. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 404: The referenced resource was not found. - 409: The write conflicts with an existing resource or the resource's current state. - 413: The request body exceeds the 10 MB limit. - 415: The request used an unsupported content type, charset, or content encoding. - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. ## Delete a record `DELETE /v1/schemas/{schema}/records/{id}` Requests durable deletion of a contact or object. Requires an Idempotency-Key identifying this explicit attempt: 1..128 characters, beginning with an alphanumeric character and containing only alphanumeric characters, underscore, period, colon or hyphen. Preserve it when recovering a lost response. The key identifies the operation; it does not use generic response-cache replay headers. The response carries operationId/status/canCancel/message. New admissions pause while accepted Events and deliveries drain; no records have been removed at that stage. Once removal begins, conflicting workspace work remains closed during cleanup. Poll GET /record-deletions/{operationId}; pending is unfinished, blocked needs reconciliation, and completed confirms erasure. Cancellation is available to the original requester with current write permission only before removal begins. This handler returns HTTP 202 for non-completed states and HTTP 200 for completed. A cancelled attempt must not be reused as a new deletion. An unknown record returns 404 record_not_found; after completion, retrying DELETE can therefore return 404, so retain the operationId for status inspection. Authentication: Bearer API key required. ### Parameters - `schema` (path, string, required): A schema id or slug. - `id` (path, string, required): The record's platform id. - `Idempotency-Key` (header, string, required): Stable identity for this explicit deletion attempt. Keep the same key to recover its operation status. This does not use generic response caching or replay headers. A new attempt after cancellation requires a new key; inspect the original operation first. ### Response example ```json { "data": { "operationId": "delete:records:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "status": "completed", "canCancel": false, "message": "Record deletion is completed." } } ``` ### Responses - 200: Record deletion is completed. - 202: Deletion is unfinished. Poll the operation until status is completed. - 400: The request was malformed or failed validation. Also covers `malformed_json` (body is not valid JSON) and, on list endpoints, `offset_too_large`. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 404: The referenced resource was not found. - 409: The write conflicts with an existing resource or the resource's current state. - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. ## List a contact's audit trail `GET /v1/schemas/{schema}/records/{id}/audit-trail` Lists the consent and communication audit trail for a contact, newest first. Contact schemas only. Errors: `schema_not_found`, `record_not_found`. Authentication: Bearer API key required. ### Parameters - `schema` (path, string, required): A schema id or slug. - `id` (path, string, required): The record's platform id. - `channel` (query, string, optional): Filter by communication channel. - `limit` (query, integer, optional): Page size. Clamped to 1..200 with a warning if out of range. - `offset` (query, integer, optional): Rows to skip. Negative values clamp to 0 with a warning; above 10000 returns 400 offset_too_large. pagination.hasMore becomes false when the next offset reaches the cap even if totalCount is larger. Records list has no ad-hoc filter; this is not an unlimited export contract. ### Response example ```json { "data": [ { "id": "cat_BcwDvBUeSaSDILA5tHgpmU7I", "createdAt": "2025-01-15T10:30:00.000Z", "communicationChannel": "email", "communicationTopicIds": [ "ct_BcwDvBUeSaSDILA5tHgpmU7I" ], "eventType": "email_verification_sent", "state": "subscribed", "optInLevel": "single", "actorType": "contact", "actorIdentifier": "string", "context": null } ], "pagination": { "limit": 0, "offset": 0, "totalCount": 0, "hasMore": false }, "warnings": [] } ``` ### Responses - 200: A page of audit-trail entries. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 404: The referenced resource was not found. - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. ## Get record deletion status `GET /v1/record-deletions/{operationId}` Poll a record-deletion operation in the caller's space. pending means drain, cancellation or cleanup is unfinished; inspect message for the current phase. blocked needs reconciliation; completed confirms deletion, and cancelled confirms cancellation before record removal. A missing or out-of-scope operation returns 404. Preserve operationId from records.delete. This read-only response reports canCancel=false; cancellation eligibility is enforced separately by records.cancelDeletion against the current requester and operation phase. Authentication: Bearer API key required. ### Parameters - `operationId` (path, string, required): The full operationId returned by records.delete. ### Response example ```json { "data": { "operationId": "delete:records:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "status": "pending", "canCancel": false, "message": "Record deletion is in progress." }, "warnings": [] } ``` ### Responses - 200: Current deletion status. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 404: The referenced resource was not found. - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. ## Cancel a record deletion before removal `POST /v1/record-deletions/{operationId}/cancel` Requests cancellation during the initial admission-drain phase, before data removal begins. Requires the original requesting user and current Write permission in the workspace. Returns 202 while collection intake retirement is acknowledged, then 200 for an already cancelled operation. Poll records.getDeletion until status=cancelled. Repeating cancellation during cancellation or after it completes returns the current operation status. There is no separate Idempotency-Key for this route: operationId identifies the existing operation. A different requester or out-of-scope operation returns 404. Removal already begun returns 409; a currently leased admission check also returns 409 and requires waiting before retrying. Cancellation cannot restore data after removal begins. Authentication: Bearer API key required. ### Parameters - `operationId` (path, string, required): The full operationId returned by records.delete. ### Response example ```json { "data": { "operationId": "delete:records:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "status": "pending", "canCancel": false, "message": "Record deletion is in progress." } } ``` ### Responses - 200: The operation was already cancelled. - 202: Cancellation is pending; poll the deletion operation. - 400: The request was malformed or failed validation. Also covers `malformed_json` (body is not valid JSON) and, on list endpoints, `offset_too_large`. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 404: The referenced resource was not found. - 409: The write conflicts with an existing resource or the resource's current state. - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. --- # Schemas API reference Source: https://docs.maxclicks.ai/api/schemas Read the space's schemas (contact, object, event). Schema definitions are authored in the app. Base URL: https://api.maxclicks.ai ## List schemas `GET /v1/schemas` Lists the space's schemas, optionally filtered by type. Authentication: Bearer API key required. ### Parameters - `type` (query, string[], optional): Filter by one or more schema types. Repeat the parameter to pass several. - `limit` (query, integer, optional): Page size. Clamped to 1..200 with a warning if out of range. - `offset` (query, integer, optional): Rows to skip. Negative values clamp to 0 with a warning; above 10000 returns 400 offset_too_large. pagination.hasMore becomes false when the next offset reaches the cap even if totalCount is larger. Records list has no ad-hoc filter; this is not an unlimited export contract. ### Response example ```json { "data": [ { "id": "sch_BcwDvBUeSaSDILA5tHgpmU7I", "type": "contact", "slug": "string", "name": "string", "namePlural": "string", "description": "string", "integrationVendorName": "string", "attributeCount": 0, "createdAt": "2025-01-15T10:30:00.000Z", "updatedAt": "2025-01-15T10:30:00.000Z" } ], "pagination": { "limit": 0, "offset": 0, "totalCount": 0, "hasMore": false }, "warnings": [] } ``` ### Responses - 200: A page of schemas. - 400: The request was malformed or failed validation. Also covers `malformed_json` (body is not valid JSON) and, on list endpoints, `offset_too_large`. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. ## Get a schema `GET /v1/schemas/{schema}` Fetches a single schema by id or slug. Authentication: Bearer API key required. ### Parameters - `schema` (path, string, required): A schema id or slug. ### Response example ```json { "data": { "id": "sch_BcwDvBUeSaSDILA5tHgpmU7I", "type": "contact", "slug": "string", "name": "string", "namePlural": "string", "description": "string", "integrationVendorName": "string", "attributeCount": 0, "createdAt": "2025-01-15T10:30:00.000Z", "updatedAt": "2025-01-15T10:30:00.000Z" }, "warnings": [] } ``` ### Responses - 200: The schema. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 404: The referenced resource was not found. - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. --- # Segments API reference Source: https://docs.maxclicks.ai/api/segments Generate and manage AI-defined contact filters. Base URL: https://api.maxclicks.ai ## List segments `GET /v1/segments` Authentication: Bearer API key required. ### Parameters - `limit` (query, integer, optional): Page size. Clamped to 1..200 with a warning if out of range. - `offset` (query, integer, optional): Rows to skip. Negative values clamp to 0 with a warning; above 10000 returns 400 offset_too_large. pagination.hasMore becomes false when the next offset reaches the cap even if totalCount is larger. Records list has no ad-hoc filter; this is not an unlimited export contract. ### Response example ```json { "data": [ { "id": "seg_BcwDvBUeSaSDILA5tHgpmU7I", "createdAt": "2025-01-15T10:30:00.000Z", "updatedAt": "2025-01-15T10:30:00.000Z", "name": "string", "description": "string", "contactSchemaId": "sch_BcwDvBUeSaSDILA5tHgpmU7I", "contactSchemaSlug": "string", "requirements": "string", "conditionDescriptionMarkdown": "string", "integrationVendorName": "string", "matchingCount": 0 } ], "pagination": { "limit": 0, "offset": 0, "totalCount": 0, "hasMore": false }, "warnings": [] } ``` ### Responses - 200: A page of segments. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. ## Create a segment `POST /v1/segments` Generates a contact filter from a natural-language `requirements` prompt (every create is an AI call, in the `ai` rate bucket; credits are checked first) and persists it in one call. The response includes a live `matchingCount`. The generated expression is never returned. Errors: `schema_not_found`, `invalid_segment`, `segment_generation_failed` (422), `segment_name_conflict` (409), `insufficient_credits` (402). Authentication: Bearer API key required. ### Parameters - `Idempotency-Key` (header, string, optional): Stable key for one logical write, maximum 256 characters. Scoped to calling API key, method, and resolved path, with a canonical request-body fingerprint. Replays recorded status/body with Idempotent-Replay=true. Failed record create/upsert requests retain status and stable error code with a constant message; detailed validation information is only in the first authorized response. Follow the code's retry policy, rather than creating a new key from the HTTP status alone. Changed input returns 422 idempotency_key_reused; active work returns 409 idempotency_conflict; unresolved work returns idempotency_outcome_unknown. Ordinary opaque keys keep their existing 24-hour completed-response retention. For record create/upsert that needs missing AI auto-fill, use rpi1.<13-digit Unix milliseconds>.. The rpi1. prefix is reserved for timed record keys and is checked before cached-response replay. Its retry window ends 24 hours after the issue time rounded up to the next minute. A clock more than five minutes ahead is refused. Expired keys are permanently refused, including after receipt cleanup. An expired or unknown result must be reconciled; do not automatically mint a new key. Unresolved financial/provider work remains retained. Erased record responses return 410 instead of replaying private data. Other idempotent operations keep their own key rules. ### Request body Required. - `name` (string, required): - `description` (string | null, optional): - `contactSchema` (string, required): A contact schema id or slug. - `requirements` (string, required): Natural-language description of the filter criteria. ### Request body example ```json { "name": "Engaged customers", "contactSchema": "customers", "requirements": "Contacts who have opened an email in the last 30 days." } ``` ### Response example ```json { "data": { "id": "seg_BcwDvBUeSaSDILA5tHgpmU7I", "createdAt": "2025-01-15T10:30:00.000Z", "updatedAt": "2025-01-15T10:30:00.000Z", "name": "string", "description": "string", "contactSchemaId": "sch_BcwDvBUeSaSDILA5tHgpmU7I", "contactSchemaSlug": "string", "requirements": "string", "conditionDescriptionMarkdown": "string", "integrationVendorName": "string", "matchingCount": 0 }, "warnings": [] } ``` ### Responses - 201: The created segment. - 400: The request was malformed or failed validation. Also covers `malformed_json` (body is not valid JSON) and, on list endpoints, `offset_too_large`. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 402: Insufficient credits (`insufficient_credits`) for an AI-generation call or a schema with AI auto-fill, or the space's email allowance is exhausted (`email_limit_reached`). - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 404: The referenced resource was not found. - 409: The write conflicts with an existing resource or the resource's current state. - 413: The request body exceeds the 10 MB limit. - 415: The request used an unsupported content type, charset, or content encoding. - 422: The request was well-formed but could not be processed, typically a failed AI-generation call or a readiness gate (which may carry `error.issues`). - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. - 503: The operation outcome could not be confirmed. Preserve Operation-Id and the original key/body; reconcile before a new operation. ## Get a segment `GET /v1/segments/{id}` Error: `segment_not_found`. Authentication: Bearer API key required. ### Parameters - `id` (path, string, required): The segment id. ### Response example ```json { "data": { "id": "seg_BcwDvBUeSaSDILA5tHgpmU7I", "createdAt": "2025-01-15T10:30:00.000Z", "updatedAt": "2025-01-15T10:30:00.000Z", "name": "string", "description": "string", "contactSchemaId": "sch_BcwDvBUeSaSDILA5tHgpmU7I", "contactSchemaSlug": "string", "requirements": "string", "conditionDescriptionMarkdown": "string", "integrationVendorName": "string", "matchingCount": 0 }, "warnings": [] } ``` ### Responses - 200: The segment. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 404: The referenced resource was not found. - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. ## Delete a segment `DELETE /v1/segments/{id}` Errors: `segment_not_found`, `segment_integration_owned` (403), `segment_in_use` (409, used as a broadcast audience, workflow trigger, or webhook condition, or referenced by other entities). Authentication: Bearer API key required. ### Parameters - `id` (path, string, required): The segment id. - `Idempotency-Key` (header, string, optional): Stable key for one logical write, maximum 256 characters. Scoped to calling API key, method, and resolved path, with a canonical request-body fingerprint. Replays recorded status/body with Idempotent-Replay=true. Failed record create/upsert requests retain status and stable error code with a constant message; detailed validation information is only in the first authorized response. Follow the code's retry policy, rather than creating a new key from the HTTP status alone. Changed input returns 422 idempotency_key_reused; active work returns 409 idempotency_conflict; unresolved work returns idempotency_outcome_unknown. Ordinary opaque keys keep their existing 24-hour completed-response retention. For record create/upsert that needs missing AI auto-fill, use rpi1.<13-digit Unix milliseconds>.. The rpi1. prefix is reserved for timed record keys and is checked before cached-response replay. Its retry window ends 24 hours after the issue time rounded up to the next minute. A clock more than five minutes ahead is refused. Expired keys are permanently refused, including after receipt cleanup. An expired or unknown result must be reconciled; do not automatically mint a new key. Unresolved financial/provider work remains retained. Erased record responses return 410 instead of replaying private data. Other idempotent operations keep their own key rules. ### Response example ```json { "data": { "id": "seg_BcwDvBUeSaSDILA5tHgpmU7I", "deleted": true }, "warnings": [] } ``` ### Responses - 200: The segment was deleted. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 404: The referenced resource was not found. - 409: The write conflicts with an existing resource or the resource's current state. - 422: The request was well-formed but could not be processed, typically a failed AI-generation call or a readiness gate (which may carry `error.issues`). - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. - 503: The operation outcome could not be confirmed. Preserve Operation-Id and the original key/body; reconcile before a new operation. ## Count a segment's matching contacts `GET /v1/segments/{id}/count` Error: `segment_not_found`. Authentication: Bearer API key required. ### Parameters - `id` (path, string, required): The segment id. ### Response example ```json { "data": { "count": 0, "total": 0 }, "warnings": [] } ``` ### Responses - 200: The live matching count and the schema's total contact count. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 404: The referenced resource was not found. - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. ## List a segment's matching contacts `GET /v1/segments/{id}/contacts` Paginated contact records currently matching the segment. `limit` is additionally hard-capped at `config.segment.maximumContactsPageSize` (100) regardless of the requested value. Error: `segment_not_found`. Authentication: Bearer API key required. ### Parameters - `id` (path, string, required): The segment id. - `limit` (query, integer, optional): Page size. Clamped to 1..200 with a warning if out of range. - `offset` (query, integer, optional): Rows to skip. Negative values clamp to 0 with a warning; above 10000 returns 400 offset_too_large. pagination.hasMore becomes false when the next offset reaches the cap even if totalCount is larger. Records list has no ad-hoc filter; this is not an unlimited export contract. ### Response example ```json { "data": [ { "id": "con_BcwDvBUeSaSDILA5tHgpmU7I", "schemaId": "sch_BcwDvBUeSaSDILA5tHgpmU7I", "schemaSlug": "string", "source": "import", "fullName": "string", "firstName": "string", "lastName": "string", "avatarUrl": "string", "userId": "user_8412", "email": "contact@example.com", "phone": "string", "subscriptions": [ "email" ], "topicIds": [ "ct_BcwDvBUeSaSDILA5tHgpmU7I" ], "notes": "string", "tags": [ "string" ], "emailStatistics": { "sent": 0, "delivered": 0, "bounced": 0, "complained": 0, "unsubscribed": 0, "uniqueOpens": 0, "uniqueClicks": 0 }, "createdAt": "2025-01-15T10:30:00.000Z", "updatedAt": "2025-01-15T10:30:00.000Z" } ], "pagination": { "limit": 0, "offset": 0, "totalCount": 0, "hasMore": false }, "warnings": [] } ``` ### Responses - 200: A page of matching contacts. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 404: The referenced resource was not found. - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. --- # Senders API reference Source: https://docs.maxclicks.ai/api/senders Read saved sender profiles configured in the app. Base URL: https://api.maxclicks.ai ## List senders `GET /v1/senders` Authentication: Bearer API key required. ### Parameters - `limit` (query, integer, optional): Page size. Clamped to 1..200 with a warning if out of range. - `offset` (query, integer, optional): Rows to skip. Negative values clamp to 0 with a warning; above 10000 returns 400 offset_too_large. pagination.hasMore becomes false when the next offset reaches the cap even if totalCount is larger. Records list has no ad-hoc filter; this is not an unlimited export contract. ### Response example ```json { "data": [ { "id": "snd_BcwDvBUeSaSDILA5tHgpmU7I", "createdAt": "2025-01-15T10:30:00.000Z", "updatedAt": "2025-01-15T10:30:00.000Z", "name": "string", "description": "string", "code": "string", "domainId": "dom_BcwDvBUeSaSDILA5tHgpmU7I", "email": "contact@example.com" } ], "pagination": { "limit": 0, "offset": 0, "totalCount": 0, "hasMore": false }, "warnings": [] } ``` ### Responses - 200: A page of senders. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. --- # Suppressions API reference Source: https://docs.maxclicks.ai/api/suppressions Manage the email do-not-contact list. Requires an admin key. Base URL: https://api.maxclicks.ai ## List suppressions `GET /v1/contacts/suppressions` Lists suppressions for the caller's space, newest first. Includes global suppressions (not space-scoped) alongside space-scoped ones, since both block sends to the caller's contacts. Requires an admin key. Authentication: Bearer API key required. ### Parameters - `identifier` (query, string (email), optional): Filter to the suppression(s) for one email address. - `limit` (query, integer, optional): Page size. Clamped to 1..200 with a warning if out of range. - `offset` (query, integer, optional): Rows to skip. Negative values clamp to 0 with a warning; above 10000 returns 400 offset_too_large. pagination.hasMore becomes false when the next offset reaches the cap even if totalCount is larger. Records list has no ad-hoc filter; this is not an unlimited export contract. ### Response example ```json { "data": [ { "id": "csp_BcwDvBUeSaSDILA5tHgpmU7I", "createdAt": "2025-01-15T10:30:00.000Z", "communicationChannel": "email", "reason": "manual", "notes": "string", "scope": "space", "identifierHash": "string", "affectedContactCount": 0 } ], "pagination": { "limit": 0, "offset": 0, "totalCount": 0, "hasMore": false }, "warnings": [] } ``` ### Responses - 200: A page of suppressions. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. ## Create a suppression `POST /v1/contacts/suppressions` Suppresses an email identifier for the space. Requires an admin key. Not Idempotency-Key aware. Error: `suppression_already_exists` (409; `manual` and `legal` are distinct reasons). Authentication: Bearer API key required. ### Request body Required. - `identifier` (string (email), required): The email address to suppress. - `reason` (string, required): Allowed values: "manual", "legal". - `notes` (string | null, optional): ### Request body example ```json { "identifier": "contact@example.com", "reason": "manual", "notes": "string" } ``` ### Response example ```json { "data": { "id": "csp_BcwDvBUeSaSDILA5tHgpmU7I", "createdAt": "2025-01-15T10:30:00.000Z", "communicationChannel": "email", "reason": "manual", "notes": "string", "scope": "space", "identifierHash": "string", "affectedContactCount": 0 }, "warnings": [] } ``` ### Responses - 201: The created suppression. - 400: The request was malformed or failed validation. Also covers `malformed_json` (body is not valid JSON) and, on list endpoints, `offset_too_large`. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 409: The write conflicts with an existing resource or the resource's current state. - 413: The request body exceeds the 10 MB limit. - 415: The request used an unsupported content type, charset, or content encoding. - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. ## Batch-create suppressions `POST /v1/contacts/suppressions/batch` Bulk-creates 1 to 100 suppressions in one call. Each item is validated and processed independently: an existing suppression for the same identifier and reason returns `exists` rather than failing the item. Requires an admin key. Supports `Idempotency-Key`. Always responds `200`. Authentication: Bearer API key required. ### Parameters - `Idempotency-Key` (header, string, optional): Stable key for one logical write, maximum 256 characters. Scoped to calling API key, method, and resolved path, with a canonical request-body fingerprint. Replays recorded status/body with Idempotent-Replay=true. Failed record create/upsert requests retain status and stable error code with a constant message; detailed validation information is only in the first authorized response. Follow the code's retry policy, rather than creating a new key from the HTTP status alone. Changed input returns 422 idempotency_key_reused; active work returns 409 idempotency_conflict; unresolved work returns idempotency_outcome_unknown. Ordinary opaque keys keep their existing 24-hour completed-response retention. For record create/upsert that needs missing AI auto-fill, use rpi1.<13-digit Unix milliseconds>.. The rpi1. prefix is reserved for timed record keys and is checked before cached-response replay. Its retry window ends 24 hours after the issue time rounded up to the next minute. A clock more than five minutes ahead is refused. Expired keys are permanently refused, including after receipt cleanup. An expired or unknown result must be reconciled; do not automatically mint a new key. Unresolved financial/provider work remains retained. Erased record responses return 410 instead of replaying private data. Other idempotent operations keep their own key rules. ### Request body Required. - `suppressions` (object[], required): ### Request body example ```json { "suppressions": [ { "identifier": "contact@example.com", "reason": "manual", "notes": "string" } ] } ``` ### Response example ```json { "data": { "results": [ { "index": 0, "status": "created", "error": { "type": "invalid_request_error", "code": "string", "message": "string", "issues": [ "string" ] }, "id": "csp_BcwDvBUeSaSDILA5tHgpmU7I", "createdAt": "2025-01-15T10:30:00.000Z", "communicationChannel": "email", "reason": "manual", "notes": "string", "scope": "space", "identifierHash": "string", "affectedContactCount": 0 } ], "summary": { "created": 0, "exists": 0, "failed": 0 } }, "warnings": [] } ``` ### Responses - 200: Per-item results and a summary. - 400: The request was malformed or failed validation. Also covers `malformed_json` (body is not valid JSON) and, on list endpoints, `offset_too_large`. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 409: The write conflicts with an existing resource or the resource's current state. - 413: The request body exceeds the 10 MB limit. - 415: The request used an unsupported content type, charset, or content encoding. - 422: The request was well-formed but could not be processed, typically a failed AI-generation call or a readiness gate (which may carry `error.issues`). - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. - 503: The operation outcome could not be confirmed. Preserve Operation-Id and the original key/body; reconcile before a new operation. ## Batch-delete suppressions `POST /v1/contacts/suppressions/batch-remove` Bulk-removes 1 to 100 suppressions by id in one call. Each id is gated exactly like the single delete (must belong to the caller's space; reason must be `manual` or `legal`). Requires an admin key. Supports `Idempotency-Key`. Always responds `200`. Authentication: Bearer API key required. ### Parameters - `Idempotency-Key` (header, string, optional): Stable key for one logical write, maximum 256 characters. Scoped to calling API key, method, and resolved path, with a canonical request-body fingerprint. Replays recorded status/body with Idempotent-Replay=true. Failed record create/upsert requests retain status and stable error code with a constant message; detailed validation information is only in the first authorized response. Follow the code's retry policy, rather than creating a new key from the HTTP status alone. Changed input returns 422 idempotency_key_reused; active work returns 409 idempotency_conflict; unresolved work returns idempotency_outcome_unknown. Ordinary opaque keys keep their existing 24-hour completed-response retention. For record create/upsert that needs missing AI auto-fill, use rpi1.<13-digit Unix milliseconds>.. The rpi1. prefix is reserved for timed record keys and is checked before cached-response replay. Its retry window ends 24 hours after the issue time rounded up to the next minute. A clock more than five minutes ahead is refused. Expired keys are permanently refused, including after receipt cleanup. An expired or unknown result must be reconciled; do not automatically mint a new key. Unresolved financial/provider work remains retained. Erased record responses return 410 instead of replaying private data. Other idempotent operations keep their own key rules. ### Request body Required. - `ids` (string[], required): ### Request body example ```json { "ids": [ "string" ] } ``` ### Response example ```json { "data": { "results": [ { "index": 0, "status": "removed", "error": { "type": "invalid_request_error", "code": "string", "message": "string", "issues": [ "string" ] } } ], "summary": { "removed": 0, "failed": 0 } }, "warnings": [] } ``` ### Responses - 200: Per-item results and a summary. - 400: The request was malformed or failed validation. Also covers `malformed_json` (body is not valid JSON) and, on list endpoints, `offset_too_large`. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 409: The write conflicts with an existing resource or the resource's current state. - 413: The request body exceeds the 10 MB limit. - 415: The request used an unsupported content type, charset, or content encoding. - 422: The request was well-formed but could not be processed, typically a failed AI-generation call or a readiness gate (which may carry `error.issues`). - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. - 503: The operation outcome could not be confirmed. Preserve Operation-Id and the original key/body; reconcile before a new operation. ## Delete a suppression `DELETE /v1/contacts/suppressions/{id}` Deletes a manual or legal suppression. Requires admin permission and supports Idempotency-Key. Missing suppression returns suppression_not_found; automatic provider bounce/complaint suppressions cannot be removed through this endpoint. Removing a suppression is separate from granting contact marketing consent. Authentication: Bearer API key required. ### Parameters - `id` (path, string, required): The suppression id. - `Idempotency-Key` (header, string, optional): Stable key for one logical write, maximum 256 characters. Scoped to calling API key, method, and resolved path, with a canonical request-body fingerprint. Replays recorded status/body with Idempotent-Replay=true. Failed record create/upsert requests retain status and stable error code with a constant message; detailed validation information is only in the first authorized response. Follow the code's retry policy, rather than creating a new key from the HTTP status alone. Changed input returns 422 idempotency_key_reused; active work returns 409 idempotency_conflict; unresolved work returns idempotency_outcome_unknown. Ordinary opaque keys keep their existing 24-hour completed-response retention. For record create/upsert that needs missing AI auto-fill, use rpi1.<13-digit Unix milliseconds>.. The rpi1. prefix is reserved for timed record keys and is checked before cached-response replay. Its retry window ends 24 hours after the issue time rounded up to the next minute. A clock more than five minutes ahead is refused. Expired keys are permanently refused, including after receipt cleanup. An expired or unknown result must be reconciled; do not automatically mint a new key. Unresolved financial/provider work remains retained. Erased record responses return 410 instead of replaying private data. Other idempotent operations keep their own key rules. ### Response example ```json { "data": { "id": "csp_BcwDvBUeSaSDILA5tHgpmU7I", "deleted": true }, "warnings": [] } ``` ### Responses - 200: The suppression was removed. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 404: The referenced resource was not found. - 409: The write conflicts with an existing resource or the resource's current state. - 422: The request was well-formed but could not be processed, typically a failed AI-generation call or a readiness gate (which may carry `error.issues`). - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. - 503: The operation outcome could not be confirmed. Preserve Operation-Id and the original key/body; reconcile before a new operation. --- # Templates API reference Source: https://docs.maxclicks.ai/api/templates Send stored message templates. Templates are authored in the app. Base URL: https://api.maxclicks.ai ## List templates `GET /v1/templates` Authentication: Bearer API key required. ### Parameters - `limit` (query, integer, optional): Page size. Clamped to 1..200 with a warning if out of range. - `offset` (query, integer, optional): Rows to skip. Negative values clamp to 0 with a warning; above 10000 returns 400 offset_too_large. pagination.hasMore becomes false when the next offset reaches the cap even if totalCount is larger. Records list has no ad-hoc filter; this is not an unlimited export contract. ### Response example ```json { "data": [ { "id": "tpl_BcwDvBUeSaSDILA5tHgpmU7I", "createdAt": "2025-01-15T10:30:00.000Z", "updatedAt": "2025-01-15T10:30:00.000Z", "name": "string", "description": "string", "transactional": false, "contentFormat": "mjml", "contentEditor": "rich", "content": { "subject": "string", "body": "string" }, "from": { "type": "domain", "domainId": "dom_BcwDvBUeSaSDILA5tHgpmU7I", "code": "string", "name": "string" }, "recipientDataPath": [ "string" ], "expectedDataRepresentation": [ { "key": "string", "type": "record", "schema": "string", "isArray": false, "description": "string" } ], "communicationTopicId": "ct_BcwDvBUeSaSDILA5tHgpmU7I" } ], "pagination": { "limit": 0, "offset": 0, "totalCount": 0, "hasMore": false }, "warnings": [] } ``` ### Responses - 200: A page of templates. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. ## Get a template `GET /v1/templates/{id}` Error: `template_not_found`. Authentication: Bearer API key required. ### Parameters - `id` (path, string, required): The template id. ### Response example ```json { "data": { "id": "tpl_BcwDvBUeSaSDILA5tHgpmU7I", "createdAt": "2025-01-15T10:30:00.000Z", "updatedAt": "2025-01-15T10:30:00.000Z", "name": "string", "description": "string", "transactional": false, "contentFormat": "mjml", "contentEditor": "rich", "content": { "subject": "string", "body": "string" }, "from": { "type": "domain", "domainId": "dom_BcwDvBUeSaSDILA5tHgpmU7I", "code": "string", "name": "string" }, "recipientDataPath": [ "string" ], "expectedDataRepresentation": [ { "key": "string", "type": "record", "schema": "string", "isArray": false, "description": "string" } ], "communicationTopicId": "ct_BcwDvBUeSaSDILA5tHgpmU7I" }, "warnings": [] } ``` ### Responses - 200: The template. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 404: The referenced resource was not found. - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. ## Send a template `POST /v1/templates/{templateId}/send` Requires Write permission and a stable Idempotency-Key. Renders stored template content and sends to the recipient resolved from data. Read expectedDataRepresentation and send every required key; referenced records are upserted before rendering and configured attribute expansions are applied. Available credits and email allowance are required. There is no subject/body replacement or preview execution parameter. A template without an email channel returns template_not_sendable. Pre-send validation and eligibility failures use request errors (email_limit_reached, sending_disabled, template_not_sendable, domain_not_verified, invalid_data, marketing_requires_contact, recipient_not_subscribed, recipient_suppressed). Records or preparation artifacts can exist before a later failure; an error does not prove that the request made no changes. HTTP 200 exposes emailId, status, and error. status=sent means the provider accepted the durable send attempt, not inbox delivery. A known terminal refusal or cancellation can return status=failed with a generic error. A temporary dispatch refusal returns 429 template_send_retryable with Retry-After. Wait, then retry the same key, template and input. The retry reuses the saved message and recipient. An uncertain delivery or rendering result returns 409 template_send_outcome_unknown without a retry hint. Reconcile it; do not create a new key to force a second send. A confirmed permanent rejection stays failed on retry. Template sends do not return generic response-cache replay headers. Authentication: Bearer API key required. ### Parameters - `templateId` (path, string, required): The template id. - `Idempotency-Key` (header, string, required): Non-blank identity for one template send. Keep the same API key, template and input when retrying. The identity is scoped to the space and calling API key; using it for a different template or input returns 409 template_send_operation_conflict. A retry resumes the saved preparation or delivery and reuses its exact message and recipient. It does not replay a generic cached HTTP response, return Idempotent-Replay or Operation-Id, or permit key reuse after the generic 24-hour response-cache period. An erased request returns 410 template_send_erased. An unknown result needs reconciliation; do not mint a replacement key. ### Request body Required. - `data` (object, required): Payload keyed by the template's expected data-representation properties. Minimal identifiers suffice for record properties (e.g. `{ "email": "user@example.com" }` for a contact); the platform finds-or-creates the record, then reads it back for rendering. ### Request body example ```json { "data": { "contact": { "email": "sarah@example.com" } } } ``` ### Response example ```json { "data": { "emailId": "email_BcwDvBUeSaSDILA5tHgpmU7I", "status": "sent", "error": "string" }, "warnings": [] } ``` ### Responses - 200: The send was processed. Inspect data.status for the delivery result. - 400: The request is invalid, including a missing or blank Idempotency-Key (template_send_operation_required). - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 402: Insufficient credits (`insufficient_credits`) for an AI-generation call or a schema with AI auto-fill, or the space's email allowance is exhausted (`email_limit_reached`). - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 404: The referenced resource was not found. - 409: template_send_operation_conflict means this key was already used with a different template or input. template_send_outcome_unknown means an existing rendering or delivery result needs reconciliation. Neither response authorizes a new send identity. - 410: The send or its source data was erased (template_send_erased). The saved operation cannot be retried. - 413: The request body exceeds the 10 MB limit. - 415: The request used an unsupported content type, charset, or content encoding. - 422: The request was well-formed but could not be processed, typically a failed AI-generation call or a readiness gate (which may carry `error.issues`). - 429: The API rate limit or dispatch capacity is temporarily unavailable. For template_send_retryable, the message can already be prepared. Wait for Retry-After, then retry the unchanged request with the same Idempotency-Key. - 500: The API failed to process a valid request. - 503: The send is still processing (template_send_processing), preparation capacity is full (template_send_capacity_in_use), or the space is paused for maintenance. Retry later with the same identity and input. An uncertain provider result instead returns 409. --- # Topics API reference Source: https://docs.maxclicks.ai/api/topics Read communication topics configured in the app. Base URL: https://api.maxclicks.ai ## List topics `GET /v1/topics` Authentication: Bearer API key required. ### Parameters - `limit` (query, integer, optional): Page size. Clamped to 1..200 with a warning if out of range. - `offset` (query, integer, optional): Rows to skip. Negative values clamp to 0 with a warning; above 10000 returns 400 offset_too_large. pagination.hasMore becomes false when the next offset reaches the cap even if totalCount is larger. Records list has no ad-hoc filter; this is not an unlimited export contract. ### Response example ```json { "data": [ { "id": "ct_BcwDvBUeSaSDILA5tHgpmU7I", "createdAt": "2025-01-15T10:30:00.000Z", "updatedAt": "2025-01-15T10:30:00.000Z", "name": "string", "description": "string", "color": "red", "channel": "email", "private": false } ], "pagination": { "limit": 0, "offset": 0, "totalCount": 0, "hasMore": false }, "warnings": [] } ``` ### Responses - 200: A page of topics. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. ## Get a topic `GET /v1/topics/{id}` Error: `topic_not_found`. Authentication: Bearer API key required. ### Parameters - `id` (path, string, required): The topic id. ### Response example ```json { "data": { "id": "ct_BcwDvBUeSaSDILA5tHgpmU7I", "createdAt": "2025-01-15T10:30:00.000Z", "updatedAt": "2025-01-15T10:30:00.000Z", "name": "string", "description": "string", "color": "red", "channel": "email", "private": false }, "warnings": [] } ``` ### Responses - 200: The topic. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 404: The referenced resource was not found. - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. --- # Webhooks API reference Source: https://docs.maxclicks.ai/api/webhooks Manage outgoing webhooks. Base URL: https://api.maxclicks.ai ## List webhooks `GET /v1/webhooks` Authentication: Bearer API key required. ### Parameters - `limit` (query, integer, optional): Page size. Clamped to 1..200 with a warning if out of range. - `offset` (query, integer, optional): Rows to skip. Negative values clamp to 0 with a warning; above 10000 returns 400 offset_too_large. pagination.hasMore becomes false when the next offset reaches the cap even if totalCount is larger. Records list has no ad-hoc filter; this is not an unlimited export contract. ### Response example ```json { "data": [ { "id": "wh_BcwDvBUeSaSDILA5tHgpmU7I", "createdAt": "2025-01-15T10:30:00.000Z", "updatedAt": "2025-01-15T10:30:00.000Z", "url": "https://example.com", "trigger": "contact upserted", "emailEventTypes": [ "scheduled" ], "schemaId": "sch_BcwDvBUeSaSDILA5tHgpmU7I", "condition": { "type": "none" }, "status": "live", "totalCalls": 0, "failedCalls": 0, "failedCallsInARow": 0, "lastCalledAt": "2025-01-15T10:30:00.000Z", "lastCallErrorMessage": "string", "integrationVendorName": "string" } ], "pagination": { "limit": 0, "offset": 0, "totalCount": 0, "hasMore": false }, "warnings": [] } ``` ### Responses - 200: A page of webhooks. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. ## Create a webhook `POST /v1/webhooks` Creates an outgoing webhook and returns signatureVerificationSecret. Store the secret securely; normal GET/list responses omit it. Supports Idempotency-Key, including replay of the original secret-bearing response. A custom filter generates through AI from requirements, uses the ai rate bucket and credits, and exposes only its description. Schema accepts an id or slug and must match the trigger. Deliveries retry network failures, HTTP 408, 429, and 5xx with bounded backoff; deduplicate their stable X-Webhook-ID. Delivery URL and secret are captured when enqueued, so queued deliveries can use earlier settings. Errors include invalid_webhook, schema_not_found, segment_not_found, and insufficient_credits. Authentication: Bearer API key required. ### Parameters - `Idempotency-Key` (header, string, optional): Stable key for one logical write, maximum 256 characters. Scoped to calling API key, method, and resolved path, with a canonical request-body fingerprint. Replays recorded status/body with Idempotent-Replay=true. Failed record create/upsert requests retain status and stable error code with a constant message; detailed validation information is only in the first authorized response. Follow the code's retry policy, rather than creating a new key from the HTTP status alone. Changed input returns 422 idempotency_key_reused; active work returns 409 idempotency_conflict; unresolved work returns idempotency_outcome_unknown. Ordinary opaque keys keep their existing 24-hour completed-response retention. For record create/upsert that needs missing AI auto-fill, use rpi1.<13-digit Unix milliseconds>.. The rpi1. prefix is reserved for timed record keys and is checked before cached-response replay. Its retry window ends 24 hours after the issue time rounded up to the next minute. A clock more than five minutes ahead is refused. Expired keys are permanently refused, including after receipt cleanup. An expired or unknown result must be reconciled; do not automatically mint a new key. Unresolved financial/provider work remains retained. Erased record responses return 410 instead of replaying private data. Other idempotent operations keep their own key rules. ### Request body Required. - `url` (string (uri), required): - `trigger` (string, required): Allowed values: "contact upserted", "contact deleted", "object upserted", "object deleted", "event fired", "email event". - `emailEventTypes` (string[], optional): Required and non-empty only when trigger is `email event`. - `schemaId` (string | null, optional): A schema id or slug. Required for every trigger except `email event`. - `condition` (object, optional): The filter condition to create or update a webhook with. A `custom filter` condition generates via AI from `requirements` (billed, `ai` rate bucket). ### Request body example ```json { "url": "https://example.com/webhooks/maxclicks", "trigger": "contact upserted", "schemaId": "customers", "condition": { "type": "none" } } ``` ### Response example ```json { "data": { "id": "wh_BcwDvBUeSaSDILA5tHgpmU7I", "createdAt": "2025-01-15T10:30:00.000Z", "updatedAt": "2025-01-15T10:30:00.000Z", "url": "https://example.com", "trigger": "contact upserted", "emailEventTypes": [ "scheduled" ], "schemaId": "sch_BcwDvBUeSaSDILA5tHgpmU7I", "condition": { "type": "none" }, "status": "live", "totalCalls": 0, "failedCalls": 0, "failedCallsInARow": 0, "lastCalledAt": "2025-01-15T10:30:00.000Z", "lastCallErrorMessage": "string", "integrationVendorName": "string", "signatureVerificationSecret": "string" }, "warnings": [] } ``` ### Responses - 201: The created webhook, including its signing secret. - 400: The request was malformed or failed validation. Also covers `malformed_json` (body is not valid JSON) and, on list endpoints, `offset_too_large`. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 402: Insufficient credits (`insufficient_credits`) for an AI-generation call or a schema with AI auto-fill, or the space's email allowance is exhausted (`email_limit_reached`). - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 404: The referenced resource was not found. - 409: The write conflicts with an existing resource or the resource's current state. - 413: The request body exceeds the 10 MB limit. - 415: The request used an unsupported content type, charset, or content encoding. - 422: The request was well-formed but could not be processed, typically a failed AI-generation call or a readiness gate (which may carry `error.issues`). - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. - 503: The operation outcome could not be confirmed. Preserve Operation-Id and the original key/body; reconcile before a new operation. ## Get a webhook `GET /v1/webhooks/{id}` Authentication: Bearer API key required. ### Parameters - `id` (path, string, required): The webhook id. ### Response example ```json { "data": { "id": "wh_BcwDvBUeSaSDILA5tHgpmU7I", "createdAt": "2025-01-15T10:30:00.000Z", "updatedAt": "2025-01-15T10:30:00.000Z", "url": "https://example.com", "trigger": "contact upserted", "emailEventTypes": [ "scheduled" ], "schemaId": "sch_BcwDvBUeSaSDILA5tHgpmU7I", "condition": { "type": "none" }, "status": "live", "totalCalls": 0, "failedCalls": 0, "failedCallsInARow": 0, "lastCalledAt": "2025-01-15T10:30:00.000Z", "lastCallErrorMessage": "string", "integrationVendorName": "string" }, "warnings": [] } ``` ### Responses - 200: The webhook. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 404: The referenced resource was not found. - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. ## Update a webhook `PATCH /v1/webhooks/{id}` Updates url, emailEventTypes, condition, and/or status (live or paused); at least one is required. Trigger and schema cannot change. A custom filter uses AI and credits. Supports Idempotency-Key. Setting live clears the circuit-breaker count. Already enqueued deliveries keep their captured URL and signing secret. Queued deliveries observed while paused are suppressed rather than held for later replay. Integration-owned webhooks cannot be changed. Authentication: Bearer API key required. ### Parameters - `id` (path, string, required): The webhook id. - `Idempotency-Key` (header, string, optional): Stable key for one logical write, maximum 256 characters. Scoped to calling API key, method, and resolved path, with a canonical request-body fingerprint. Replays recorded status/body with Idempotent-Replay=true. Failed record create/upsert requests retain status and stable error code with a constant message; detailed validation information is only in the first authorized response. Follow the code's retry policy, rather than creating a new key from the HTTP status alone. Changed input returns 422 idempotency_key_reused; active work returns 409 idempotency_conflict; unresolved work returns idempotency_outcome_unknown. Ordinary opaque keys keep their existing 24-hour completed-response retention. For record create/upsert that needs missing AI auto-fill, use rpi1.<13-digit Unix milliseconds>.. The rpi1. prefix is reserved for timed record keys and is checked before cached-response replay. Its retry window ends 24 hours after the issue time rounded up to the next minute. A clock more than five minutes ahead is refused. Expired keys are permanently refused, including after receipt cleanup. An expired or unknown result must be reconciled; do not automatically mint a new key. Unresolved financial/provider work remains retained. Erased record responses return 410 instead of replaying private data. Other idempotent operations keep their own key rules. ### Request body Required. At least one field must be provided. Trigger and schema are not updatable. - `url` (string (uri), optional): - `emailEventTypes` (string[], optional): - `condition` (object, optional): The filter condition to create or update a webhook with. A `custom filter` condition generates via AI from `requirements` (billed, `ai` rate bucket). - `status` (string, optional): Allowed values: "live", "paused". ### Request body example ```json { "status": "live" } ``` ### Response example ```json { "data": { "id": "wh_BcwDvBUeSaSDILA5tHgpmU7I", "createdAt": "2025-01-15T10:30:00.000Z", "updatedAt": "2025-01-15T10:30:00.000Z", "url": "https://example.com", "trigger": "contact upserted", "emailEventTypes": [ "scheduled" ], "schemaId": "sch_BcwDvBUeSaSDILA5tHgpmU7I", "condition": { "type": "none" }, "status": "live", "totalCalls": 0, "failedCalls": 0, "failedCallsInARow": 0, "lastCalledAt": "2025-01-15T10:30:00.000Z", "lastCallErrorMessage": "string", "integrationVendorName": "string" }, "warnings": [] } ``` ### Responses - 200: The updated webhook. - 400: The request was malformed or failed validation. Also covers `malformed_json` (body is not valid JSON) and, on list endpoints, `offset_too_large`. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 402: Insufficient credits (`insufficient_credits`) for an AI-generation call or a schema with AI auto-fill, or the space's email allowance is exhausted (`email_limit_reached`). - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 404: The referenced resource was not found. - 409: The write conflicts with an existing resource or the resource's current state. - 413: The request body exceeds the 10 MB limit. - 415: The request used an unsupported content type, charset, or content encoding. - 422: The request was well-formed but could not be processed, typically a failed AI-generation call or a readiness gate (which may carry `error.issues`). - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. - 503: The operation outcome could not be confirmed. Preserve Operation-Id and the original key/body; reconcile before a new operation. ## Delete a webhook `DELETE /v1/webhooks/{id}` Deletes a webhook. Supports Idempotency-Key. Missing webhooks return webhook_not_found; integration-owned webhooks cannot be deleted. Authentication: Bearer API key required. ### Parameters - `id` (path, string, required): The webhook id. - `Idempotency-Key` (header, string, optional): Stable key for one logical write, maximum 256 characters. Scoped to calling API key, method, and resolved path, with a canonical request-body fingerprint. Replays recorded status/body with Idempotent-Replay=true. Failed record create/upsert requests retain status and stable error code with a constant message; detailed validation information is only in the first authorized response. Follow the code's retry policy, rather than creating a new key from the HTTP status alone. Changed input returns 422 idempotency_key_reused; active work returns 409 idempotency_conflict; unresolved work returns idempotency_outcome_unknown. Ordinary opaque keys keep their existing 24-hour completed-response retention. For record create/upsert that needs missing AI auto-fill, use rpi1.<13-digit Unix milliseconds>.. The rpi1. prefix is reserved for timed record keys and is checked before cached-response replay. Its retry window ends 24 hours after the issue time rounded up to the next minute. A clock more than five minutes ahead is refused. Expired keys are permanently refused, including after receipt cleanup. An expired or unknown result must be reconciled; do not automatically mint a new key. Unresolved financial/provider work remains retained. Erased record responses return 410 instead of replaying private data. Other idempotent operations keep their own key rules. ### Response example ```json { "data": { "id": "wh_BcwDvBUeSaSDILA5tHgpmU7I", "deleted": true }, "warnings": [] } ``` ### Responses - 200: The webhook was deleted. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 404: The referenced resource was not found. - 409: The write conflicts with an existing resource or the resource's current state. - 422: The request was well-formed but could not be processed, typically a failed AI-generation call or a readiness gate (which may carry `error.issues`). - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. - 503: The operation outcome could not be confirmed. Preserve Operation-Id and the original key/body; reconcile before a new operation. ## Rotate a webhook secret `POST /v1/webhooks/{id}/rotate-secret` Creates a new signing secret and returns it with the webhook id and updatedAt. Store it securely. Supports Idempotency-Key: retry the same key to recover the original secret-bearing response instead of rotating again. Normal reads never reveal the secret. Already enqueued deliveries retain their captured secret and URL, including retries; keep the previous verification secret until those deliveries drain. Integration-owned webhooks cannot be rotated. Authentication: Bearer API key required. ### Parameters - `id` (path, string, required): The webhook id. - `Idempotency-Key` (header, string, optional): Stable key for one logical write, maximum 256 characters. Scoped to calling API key, method, and resolved path, with a canonical request-body fingerprint. Replays recorded status/body with Idempotent-Replay=true. Failed record create/upsert requests retain status and stable error code with a constant message; detailed validation information is only in the first authorized response. Follow the code's retry policy, rather than creating a new key from the HTTP status alone. Changed input returns 422 idempotency_key_reused; active work returns 409 idempotency_conflict; unresolved work returns idempotency_outcome_unknown. Ordinary opaque keys keep their existing 24-hour completed-response retention. For record create/upsert that needs missing AI auto-fill, use rpi1.<13-digit Unix milliseconds>.. The rpi1. prefix is reserved for timed record keys and is checked before cached-response replay. Its retry window ends 24 hours after the issue time rounded up to the next minute. A clock more than five minutes ahead is refused. Expired keys are permanently refused, including after receipt cleanup. An expired or unknown result must be reconciled; do not automatically mint a new key. Unresolved financial/provider work remains retained. Erased record responses return 410 instead of replaying private data. Other idempotent operations keep their own key rules. ### Response example ```json { "data": { "id": "wh_BcwDvBUeSaSDILA5tHgpmU7I", "signatureVerificationSecret": "string", "updatedAt": "2025-01-15T10:30:00.000Z" }, "warnings": [] } ``` ### Responses - 200: The new signing secret. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 404: The referenced resource was not found. - 409: The write conflicts with an existing resource or the resource's current state. - 422: The request was well-formed but could not be processed, typically a failed AI-generation call or a readiness gate (which may carry `error.issues`). - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. - 503: The operation outcome could not be confirmed. Preserve Operation-Id and the original key/body; reconcile before a new operation. --- # Workflows API reference Source: https://docs.maxclicks.ai/api/workflows Trigger published journeys, pause execution, and inspect runs and paged step history. Base URL: https://api.maxclicks.ai ## List workflows `GET /v1/workflows` Authentication: Bearer API key required. ### Parameters - `limit` (query, integer, optional): Page size. Clamped to 1..200 with a warning if out of range. - `offset` (query, integer, optional): Rows to skip. Negative values clamp to 0 with a warning; above 10000 returns 400 offset_too_large. pagination.hasMore becomes false when the next offset reaches the cap even if totalCount is larger. Records list has no ad-hoc filter; this is not an unlimited export contract. ### Response example ```json { "data": [ { "id": "wf_BcwDvBUeSaSDILA5tHgpmU7I", "createdAt": "2025-01-15T10:30:00.000Z", "updatedAt": "2025-01-15T10:30:00.000Z", "name": "string", "description": "string", "paused": false, "isDirty": false, "published": false } ], "pagination": { "limit": 0, "offset": 0, "totalCount": 0, "hasMore": false }, "warnings": [] } ``` ### Responses - 200: A page of workflows. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. ## Get a workflow `GET /v1/workflows/{id}` Error: `workflow_not_found`. Authentication: Bearer API key required. ### Parameters - `id` (path, string, required): The workflow id. ### Response example ```json { "data": { "id": "wf_BcwDvBUeSaSDILA5tHgpmU7I", "createdAt": "2025-01-15T10:30:00.000Z", "updatedAt": "2025-01-15T10:30:00.000Z", "name": "string", "description": "string", "paused": false, "isDirty": false, "published": false }, "warnings": [] } ``` ### Responses - 200: The workflow. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 404: The referenced resource was not found. - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. ## Trigger a workflow `POST /v1/workflows/{id}` Starts a run at a published, unpaused incoming-webhook step. The path id is that step’s reference id, not the workflow id. Send the workflow’s configured JSON input as the body. HTTP 200 with data={} confirms run creation, not workflow completion. Errors use the standard JSON error envelope: workflow_step_not_found (404), invalid_request_body (400), or a server error for an invalid configured schema. Supports Idempotency-Key. Workflow authoring, run cancellation, and retry/redrive are not exposed by this public endpoint. Authentication: Bearer API key required. ### Parameters - `id` (path, string, required): The incoming-webhook step's reference id. - `Idempotency-Key` (header, string, optional): Stable key for one logical write, maximum 256 characters. Scoped to calling API key, method, and resolved path, with a canonical request-body fingerprint. Replays recorded status/body with Idempotent-Replay=true. Failed record create/upsert requests retain status and stable error code with a constant message; detailed validation information is only in the first authorized response. Follow the code's retry policy, rather than creating a new key from the HTTP status alone. Changed input returns 422 idempotency_key_reused; active work returns 409 idempotency_conflict; unresolved work returns idempotency_outcome_unknown. Ordinary opaque keys keep their existing 24-hour completed-response retention. For record create/upsert that needs missing AI auto-fill, use rpi1.<13-digit Unix milliseconds>.. The rpi1. prefix is reserved for timed record keys and is checked before cached-response replay. Its retry window ends 24 hours after the issue time rounded up to the next minute. A clock more than five minutes ahead is refused. Expired keys are permanently refused, including after receipt cleanup. An expired or unknown result must be reconciled; do not automatically mint a new key. Unresolved financial/provider work remains retained. Erased record responses return 410 instead of replaying private data. Other idempotent operations keep their own key rules. ### Request body Required. Per-workflow-step payload, validated against the step's JSON schema. ### Request body example ```json {} ``` ### Response example ```json { "data": {}, "warnings": [] } ``` ### Responses - 200: The workflow run was started. - 400: The request was malformed or failed validation. Also covers `malformed_json` (body is not valid JSON) and, on list endpoints, `offset_too_large`. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 404: The referenced resource was not found. - 409: The write conflicts with an existing resource or the resource's current state. - 413: The request body exceeds the 10 MB limit. - 415: The request used an unsupported content type, charset, or content encoding. - 422: The request was well-formed but could not be processed, typically a failed AI-generation call or a readiness gate (which may carry `error.issues`). - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. - 503: The operation outcome could not be confirmed. Preserve Operation-Id and the original key/body; reconcile before a new operation. ## Pause a workflow `POST /v1/workflows/{id}/pause` Pauses the workflow: new triggers and further execution of existing runs are blocked. An external effect already in flight may complete. Supports Idempotency-Key. Error: workflow_not_found. Authentication: Bearer API key required. ### Parameters - `id` (path, string, required): The workflow id. - `Idempotency-Key` (header, string, optional): Stable key for one logical write, maximum 256 characters. Scoped to calling API key, method, and resolved path, with a canonical request-body fingerprint. Replays recorded status/body with Idempotent-Replay=true. Failed record create/upsert requests retain status and stable error code with a constant message; detailed validation information is only in the first authorized response. Follow the code's retry policy, rather than creating a new key from the HTTP status alone. Changed input returns 422 idempotency_key_reused; active work returns 409 idempotency_conflict; unresolved work returns idempotency_outcome_unknown. Ordinary opaque keys keep their existing 24-hour completed-response retention. For record create/upsert that needs missing AI auto-fill, use rpi1.<13-digit Unix milliseconds>.. The rpi1. prefix is reserved for timed record keys and is checked before cached-response replay. Its retry window ends 24 hours after the issue time rounded up to the next minute. A clock more than five minutes ahead is refused. Expired keys are permanently refused, including after receipt cleanup. An expired or unknown result must be reconciled; do not automatically mint a new key. Unresolved financial/provider work remains retained. Erased record responses return 410 instead of replaying private data. Other idempotent operations keep their own key rules. ### Response example ```json { "data": { "id": "wf_BcwDvBUeSaSDILA5tHgpmU7I", "createdAt": "2025-01-15T10:30:00.000Z", "updatedAt": "2025-01-15T10:30:00.000Z", "name": "string", "description": "string", "paused": false, "isDirty": false, "published": false }, "warnings": [] } ``` ### Responses - 200: The paused workflow. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 404: The referenced resource was not found. - 409: The write conflicts with an existing resource or the resource's current state. - 422: The request was well-formed but could not be processed, typically a failed AI-generation call or a readiness gate (which may carry `error.issues`). - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. - 503: The operation outcome could not be confirmed. Preserve Operation-Id and the original key/body; reconcile before a new operation. ## Unpause a workflow `POST /v1/workflows/{id}/unpause` Unpauses the workflow so eligible paused work can continue. This does not publish unpublished edits or redrive terminal failed runs. Supports Idempotency-Key. Error: workflow_not_found. Authentication: Bearer API key required. ### Parameters - `id` (path, string, required): The workflow id. - `Idempotency-Key` (header, string, optional): Stable key for one logical write, maximum 256 characters. Scoped to calling API key, method, and resolved path, with a canonical request-body fingerprint. Replays recorded status/body with Idempotent-Replay=true. Failed record create/upsert requests retain status and stable error code with a constant message; detailed validation information is only in the first authorized response. Follow the code's retry policy, rather than creating a new key from the HTTP status alone. Changed input returns 422 idempotency_key_reused; active work returns 409 idempotency_conflict; unresolved work returns idempotency_outcome_unknown. Ordinary opaque keys keep their existing 24-hour completed-response retention. For record create/upsert that needs missing AI auto-fill, use rpi1.<13-digit Unix milliseconds>.. The rpi1. prefix is reserved for timed record keys and is checked before cached-response replay. Its retry window ends 24 hours after the issue time rounded up to the next minute. A clock more than five minutes ahead is refused. Expired keys are permanently refused, including after receipt cleanup. An expired or unknown result must be reconciled; do not automatically mint a new key. Unresolved financial/provider work remains retained. Erased record responses return 410 instead of replaying private data. Other idempotent operations keep their own key rules. ### Response example ```json { "data": { "id": "wf_BcwDvBUeSaSDILA5tHgpmU7I", "createdAt": "2025-01-15T10:30:00.000Z", "updatedAt": "2025-01-15T10:30:00.000Z", "name": "string", "description": "string", "paused": false, "isDirty": false, "published": false }, "warnings": [] } ``` ### Responses - 200: The unpaused workflow. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 404: The referenced resource was not found. - 409: The write conflicts with an existing resource or the resource's current state. - 422: The request was well-formed but could not be processed, typically a failed AI-generation call or a readiness gate (which may carry `error.issues`). - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. - 503: The operation outcome could not be confirmed. Preserve Operation-Id and the original key/body; reconcile before a new operation. ## List a workflow's runs `GET /v1/workflows/{id}/runs` Lists runs newest first. Each run embeds at most its latest 100 history entries, ascending within that window. Use workflows.getRun with nextHistoryBeforeIndex to read earlier history. Error: workflow_not_found. Authentication: Bearer API key required. ### Parameters - `id` (path, string, required): The workflow id. - `limit` (query, integer, optional): Page size. Clamped to 1..200 with a warning if out of range. - `offset` (query, integer, optional): Rows to skip. Negative values clamp to 0 with a warning; above 10000 returns 400 offset_too_large. pagination.hasMore becomes false when the next offset reaches the cap even if totalCount is larger. Records list has no ad-hoc filter; this is not an unlimited export contract. ### Response example ```json { "data": [ { "id": "wfr_BcwDvBUeSaSDILA5tHgpmU7I", "createdAt": "2026-09-14T10:30:00.000Z", "updatedAt": "2026-09-14T10:30:01.000Z", "versionId": "wfv_BcwDvBUeSaSDILA5tHgpmU7I", "status": "running", "failureReason": null, "stepHistory": [ { "index": 0, "stepId": "wfs_BcwDvBUeSaSDILA5tHgpmU7I", "stepLabel": "Incoming webhook", "slotKey": "next", "enteredAt": "2026-09-14T10:30:00.000Z", "leftAt": "2026-09-14T10:30:01.000Z", "failureReason": null } ], "stepCount": 1, "parentRunId": null, "parentExecutionOrdinal": null, "nextHistoryBeforeIndex": null } ], "pagination": { "limit": 0, "offset": 0, "totalCount": 0, "hasMore": false }, "warnings": [] } ``` ### Responses - 200: A page of workflow runs. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 404: The referenced resource was not found. - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. ## Get a workflow run `GET /v1/workflows/{id}/runs/{runId}` Returns public run status and a window of at most 100 step-history records, ordered by index ascending within the returned window. The initial window contains the latest records. To read older history, pass nextHistoryBeforeIndex as history_before until it is null. status is running while a current step exists; otherwise failed if the run has a failed step, or completed. failureReason comes from the returned window and can be null even for a failed run whose failure is older. No internal step graph, handling state, or input/output data is returned. Errors: workflow_not_found, workflow_run_not_found. Authentication: Bearer API key required. ### Parameters - `id` (path, string, required): The workflow id. - `runId` (path, string, required): The workflow run id. - `history_before` (query, integer, optional): Return step-history indices strictly below this non-negative safe integer. Use the previous nextHistoryBeforeIndex; omit for the latest 100 entries. ### Response example ```json { "data": { "id": "wfr_BcwDvBUeSaSDILA5tHgpmU7I", "createdAt": "2026-09-14T10:30:00.000Z", "updatedAt": "2026-09-14T10:30:01.000Z", "versionId": "wfv_BcwDvBUeSaSDILA5tHgpmU7I", "status": "running", "failureReason": null, "stepHistory": [ { "index": 0, "stepId": "wfs_BcwDvBUeSaSDILA5tHgpmU7I", "stepLabel": "Incoming webhook", "slotKey": "next", "enteredAt": "2026-09-14T10:30:00.000Z", "leftAt": "2026-09-14T10:30:01.000Z", "failureReason": null } ], "stepCount": 1, "parentRunId": null, "parentExecutionOrdinal": null, "nextHistoryBeforeIndex": null }, "warnings": [] } ``` ### Responses - 200: The workflow run. - 400: The request was malformed or failed validation. Also covers `malformed_json` (body is not valid JSON) and, on list endpoints, `offset_too_large`. - 401: The credential is missing or invalid: `invalid_api_key` for an API key, `invalid_access_token` for an expired or revoked OAuth access token. The `WWW-Authenticate` header names the resource metadata, from which an OAuth client finds the authorization server. - 403: The key lacks permission, or the space is out of scope. An OAuth connection whose user holds the permission but did not grant it answers `insufficient_scope`. - 404: The referenced resource was not found. - 429: A request rate or capacity limit was reached. Follow the error code and Retry-After; a capacity refusal can follow durable preparation. - 500: The API failed to process a valid request. --- # Attribute Expansion Source: https://docs.maxclicks.ai/attribute-expansion # Attribute expansion Expansion selects the extra customer context an email or workflow will receive. It covers both [evaluated attributes](/evaluated-attributes), which run a relationship or calculation on demand, and [scheduled evaluated attributes](/attributes#computed-attributes), which read a published background result. Suppose Customer has an evaluated `account` relationship. Maya's own name and email are already in the email data. Selecting `account` also supplies the matching Account object's stored name and plan. If you need an evaluated field on that account, select that nested field too. * **Greet Maya:** use the contact's stored `firstName`. * **Mention Studio North's plan:** select the contact's `account` relationship, then use that account's stored name and plan. * **Refer to a published activity count:** select the relevant scheduled evaluated attribute to read its selected publication. The keys above are illustrative custom fields. Use the exact paths your editor shows. Another email to Maya can select different context without changing her record or the relationship definition. ## Turn one on for an email Open the email's **Data** panel and expand its record property. Stored writable fields are part of the record. Fields that need expansion have checkboxes; select the ones your content uses. An unselected attribute is absent from the rendering context. Its result then depends on your Liquid fallback or AI instruction, rather than on the underlying customer data. Merely mentioning the key in an email does not select it. The same choice applies to [templates](/templates), [broadcasts](/broadcast) and a workflow's **Send Email** step. Use the record schema in the Data tab to confirm the exact path after selecting it. ## Go one level deeper An evaluated relationship that returns records can be expanded further. Selecting `subscription` exposes the subscription's stored fields. Select one of its evaluated or scheduled evaluated attributes if you also need that value. A relationship returning many records gives an array. Define a useful order and limit, such as “the three most recent completed orders, newest first,” then loop through those records in the email. A single-record relationship gives one record or no match. A JSON result is a value rather than another branch of record relationships. Dependencies required by an evaluated field are resolved automatically. They are not added as extra output fields unless you also selected them. ## Check it returns what you think Open a real contact or object and use the field's **Compute value** action to inspect it. For an evaluated attribute, this runs its current definition. For a scheduled evaluated attribute, reading the field retrieves its published calculation; it does not force a new calculation of event history. In the editor, select a real record as sample data and load the selected expansions. The resulting preview checks actual available values for that record. **Generate with AI** and manually entered sample data are useful for layout and fallback testing, but do not prove that your source records or relationships are correct. Test a typical customer, one with no matching history, and one with several related records. For scheduled evaluated attributes, also check **Data through** in the attribute's publication status. ## In a workflow, maxinja sets it for you When maxinja generates a step's logic, it records the fields that logic needs. Review the step's **Reads** to see the context it will request. The **AI** and **HTTP Request** steps also let you choose **Expected data** explicitly, while an email has its own data selection. Evaluated relationships can read changed records at a later step. Scheduled evaluated attributes follow a different rule: a workflow root run and its child runs share the published-generation selection frozen for that run. A newer background publication does not silently change that selection. ## Know how fresh the value is | Expanded field | What the read returns | What to check | | -------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | Evaluated | A calculation against the records currently available to maxclicks | Whether the source system has supplied the latest data, and whether the relationship/filter is correct | | Scheduled evaluation | The publication selected for the decision, with its definition revision and freshness policy | Whether a complete publication exists and meets the cadence; paused definitions can use a retained last publication | A broadcast revision and a workflow root run pin their scheduled evaluated publications, definition revisions and freshness policy when that consumer first establishes its read context. That selection includes attributes with no publication yet. Later publications do not fill them in for the existing consumer, and an erased or unavailable pinned generation fails the read rather than switching to a newer one. Freshness is assessed against the captured decision time. A standalone sample can therefore differ from an already running campaign. A definition replacement that invalidates the captured selection requires the existing decision to be resolved; it cannot switch silently to new logic. A valid scheduled publication with no matching subject value resolves to empty. No complete publication is unavailable and fails a decision that requests it. An enabled definition with an expired publication also fails a new decision; it does not silently pass an old value to the email. A paused definition with a retained publication can read its last published result. Empty history, unavailable calculation and a measured zero are different states. An on-demand one-record relationship with no match returns no record, so give that genuine no-match case a fallback. An API template send can upsert the record data in its input before loading the selected expansions. Supplying a first name along with an identifier may therefore update the contact. Send only the identity fields when your intent is to look up existing data. ## What it costs Generating an evaluated definition uses AI; executing that definition is a data lookup. Nested relationships increase the number of reads. A scheduled evaluated attribute reads a published result instead of rerunning its event calculation for every recipient. Choose the context the message needs. Narrow a relationship in its definition instead of fetching every record and discarding most in the template. Many-record relationships are capped at **1,000 records**, with excess results truncated and a warning. Expansion can fail when a generated definition is missing or invalid, a JSON result does not match its declared shape, or a dependency chain loops back on itself. Review the failing field and definition instead of treating every error as an empty match. A busy lookup service can also time out; inspect the operation's error before retrying. ## Where evaluated values do not appear Plain public API record reads and listings return supported stored record data. They do not expose evaluated relationships or scheduled-attribute expansion switches. Use the configured email and workflow data flows described here, and consult the [Records API](/api/records) for its actual response contract. --- # Attributes Source: https://docs.maxclicks.ai/attributes # Attributes Attributes give your team a shared vocabulary for customer decisions: plan, renewal date, last purchase or spend over the last 30 days. Each belongs to a [schema](/schemas) and has a defined value type. The key decision is where its value comes from. | Kind | Use it when | How the value changes | | ----------------------------- | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | | Stored field | Your product, import or integration knows the fact, such as a plan or renewal date | A record write supplies it; a missing-value policy can fill it at creation | | On-demand evaluated attribute | You need a related record or a calculation from the data currently available | Generated logic runs when the field is [expanded](/attribute-expansion) | | Scheduled evaluated attribute | You need a recurring measure of event history, such as 30-day purchase count | A background calculation publishes a value on a schedule; expansion reads that published result | For a renewal campaign, store the renewal date on the Subscription object, use an evaluated relationship on Customer to find that subscription, and use a scheduled evaluated attribute for recent customer activity. One fact belongs on the thing it describes; the relationship makes it usable from the person. For example, changing a Subscription's stored renewal date changes what a later evaluated lookup reads. It does not itself publish a new activity count or rewrite an email already prepared. Choose the value source before configuring its type. ## Add a field to a schema Open **Settings → Schemas**, choose a contact or object schema, then open **Attributes → Create attribute**. For business event schemas, open **Settings → Events** and the event's **Attributes** tab. Choose **Stored value** or **Evaluated attribute** under **How is the value set?**. For a stored field, select its **Value type**. Give it a **Name** people will recognize, a **Key** for templates and integrations, and a **Description**. Explain units and business meaning in the description: “Monthly subscription amount in EUR, excluding tax” is more useful than “Amount.” maxinja uses this context when generating audiences and logic. The key stays fixed after creation. Changes to a stored field's value type or accepted values go through setup review, which checks existing values and saved consumers before applying the new rules. It must be unique within the schema and cannot reuse a built-in field. Use a stable name such as `renewalDate`; changing the display name later does not change references to that key. ## Pick the right type | Type | Suitable for | Controls that affect the value | | ---------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ | | Text | Names, categories, descriptions | Single or multi-line; fixed options for a dropdown or radio group; code format and language for multi-line content | | Email or Phone | A contact detail held in a custom field | Format validation and normalization | | URL | Web pages, files or images | Target type; optional file or image preview | | Number | Counts, quantities, ratios | Integer only, percentage, or minimum and maximum bounds | | Decimal | Exact amounts such as money | Optional currency | | Checkbox | True or false | Checkbox or dropdown presentation | | Timestamp, Date, Time | A moment, calendar date or time of day | Choose the precision your decision needs | | Multi-select | Several text values | Optional fixed choices; multi-select dropdown or checkboxes | | Address or Geolocation | Structured location information | The field's expected address or location shape | | JSON | Nested properties, arrays or other structured data | A JSON schema defines allowed values and required properties | | Evaluated attribute | A related record, an on-demand answer or a scheduled event measure | Generated logic determines its output; review whether evaluation is on demand or scheduled | A custom email field does not replace the contact's built-in `email` identity or sending address. A JSON field does not create a relationship just because it contains an ID. Model those connections explicitly. Validation applies to writes from the app, API, forms and imports. Read the error for the field that failed, then correct the source or the schema. A field with a fixed option list accepts its configured values, not any similarly worded label. ## Decide what happens when a value is missing The stored-field form asks **When a record is created without a value**: | Choice | What happens on creation | | ----------------------- | -------------------------------------------------- | | **Leave it empty** | Save the record without a value | | **Use a default value** | Supply the fixed value you choose | | **Require it** | Reject the record and report the missing attribute | | **AI autofill** | Generate a value from the rest of that record | AI autofill offers **Infer from the record** and **Follow a prompt**. A prompt should specify the intended output and what to do with weak evidence. The resulting value must still pass the field's type validation. These policies apply when a record is created. They do not fill old records retroactively and do not run on an update that omits or clears the field. An explicitly supplied value takes precedence. An explicit empty value is still checked against whether the field accepts emptiness. AI autofill is paid generation for each new record and missing AI field that needs it. It is not an ongoing enrichment subscription. An import that matches existing contacts does not regenerate their missing fields. Use a fixed default for a fixed answer, and test a small import before scaling paid enrichment. Adding a required stored field to a populated contact or object schema needs an explicit value for existing records. A future creation default does not satisfy that backfill. Use **Fill existing empty records**, or make the field optional. Event occurrences cannot be backfilled: adding a required event field to recorded history is blocked; keep it optional or create a new event type for the new contract. ## Fields maxclicks works out for you Choose **Evaluated attribute** under **How is the value set?** when you need a reusable relationship, answer or scheduled measure. For example: “Find the Subscription whose external ID equals this customer's subscription ID.” Review the generated target, whether it returns one record or many, and its readable definition. See [evaluated attributes](/evaluated-attributes). A generated definition has a timing rule as well as a value type. An on-demand relationship runs when selected. An event-history measure publishes a result on its saved schedule. Review the generated timing before saving; a rolling measure is not recomputed for every email. ## Scheduled evaluated attributes Open the destination contact or object schema under **Settings → Schemas → Attributes → Create attribute**. Choose **Evaluated attribute** under **How is the value set?** and describe the event history it should measure. The result belongs to that profile schema; event schemas themselves are not destinations for profile values. Describe the source, measure, time window and refresh schedule. For example: > For Customer profiles, count PurchaseCompleted events explicitly linked to that customer in the last 30 days. Include only events whose status is completed. Update every hour. The current calculation supports one event schema as its source, including its configured browser or server collection history, with: * **Count**, **sum** of a numeric property, or the **latest** property value by occurrence time. * Typed filters on available properties, including comparisons, presence checks and AND/OR groups. * An optional grouping property, which produces a JSON array of grouped values. * A rolling window from **60 seconds to 30 days** and an update cadence from **one hour to 30 days**. If unspecified, generation proposes a **30-day window** with **hourly updates**. The source must identify the contact or object it describes. A custom property containing an email address or an order ID is not a substitute for the explicit subject link. The calculation does not invent a join through unrelated records. For a more complex relationship, use an evaluated attribute or prepare the required fact in your source system. ### Review before creating Generate the definition, then check its explanation, inferred type, destination schema, event source, filters, window and cadence. The field key identifies the attribute in content and integrations; it is not an authentication token. For business event schemas, **Preview values** samples up to five existing profiles. It shows the window's end and **No matching value** when no qualifying events exist. A preview does not save the attribute or publish a background result. A calculation over collected event history requires setup review for its preview. Follow the reported review path; an unavailable direct preview does not mean no activity exists. Refine an incorrect definition and generate it again before saving. Typing an instruction alone does not apply it. Count produces an integer, sum produces a decimal, and latest follows the source property's type. Changing an existing scheduled definition requires validation of the replacement result and its dependents through setup review. Current calculation limits allow up to 10 definitions per workspace and up to 20 grouping values. A preview can also reject a sample that is too large. Narrow the window or grouping when the error asks for it; a preview is a bounded validation step, not an export of the entire audience. ### Understand publication and freshness The **Published value** panel shows **Data through**, **Last completed** and **Published results**. “Data through” is the end of the measured event window; it is the timestamp to compare with your campaign's decision, rather than the time you opened the record. | Status | What to do | | -------------------------------- | ----------------------------------------------------------------------------------------------------- | | **Waiting for first evaluation** | Wait for a published result before relying on the field | | **Evaluating** | A refresh is in progress; the previous published result may still be available | | **Updated** | A published result is within its expected refresh interval | | **Delayed** | The result is older than its cadence plus a short grace period; inspect the source and refresh status | | **Updates paused** | Enable updates when you want scheduled calculations to continue | | **Needs attention** | Read the reported source or calculation error and correct it | **Refresh now** queues a calculation. It does not synchronously replace the value. Check the published timestamp after the calculation completes. A valid publication with no matching history for a profile produces an empty value, **not zero**. No complete publication is unavailable and blocks a decision that requests it. An enabled attribute also blocks a new decision when its publication is past its cadence plus a one-minute grace period. The last publication may remain visible for inspection; visibility does not establish readiness. A paused attribute with a retained publication can use that last result. Write fallbacks for genuinely missing history separately from unavailable or expired calculations. Broadcast revisions and workflow root runs hold a selected generation for consistent reads; a newer publication does not automatically replace their in-progress context. See [expansion freshness](/attribute-expansion#know-how-fresh-the-value-is) before using a current profile preview to explain an older run. These are read-only calculated fields: imports and API record writes cannot set them. They must be selected through [attribute expansion](/attribute-expansion) for email and workflow data. A [Evaluated attribute threshold trigger](/workflows) can react to published values crossing a configured condition; review that trigger's baseline and transition rules before activating it. ## What you can change later, and what you cannot Open an attribute to change its name, description and supported display settings. The key and choice between stored and evaluated values remain fixed. Stored value types, required rules, option lists, bounds, defaults and evaluated definitions can affect existing data or saved consumers, so saving them starts a setup check. For a stored field, **Existing records** makes the conversion explicit: | Choice | Effect on records already stored | | ----------------------------- | ------------------------------------------------------------------------ | | **Keep existing values** | Preserve each value; block the change if any value fails the new rules | | **Fill empty values** | Supply the chosen constant only where the field is missing or empty | | **Map specific values** | Replace exact old values; choose how unmatched values are handled | | **Convert to the new format** | Normalize existing values against the proposed type and validation rules | | **Replace all values** | Replace the field in every record, including records with a value | For example, turning free-text `planTier` into a fixed option list can expose `professional`, `Pro` and `pro` as different existing values. Map those exact values to `pro`, inspect the review examples, and check the affected audience before applying. Keeping them unchanged only succeeds if all fit the new option list. Even widening the accepted range needs review because a saved condition or template may assume a narrower domain. When creating a stored field, **Fill existing empty records** is a separate explicit backfill. It fills a constant on existing records. The creation-time default still governs future new records; neither option automatically copies a newly selected default into every old record. A successful save can apply a change after its checks pass. Changes that need a decision appear under the schema's **Changes** tab. The **Changes** link at the top of **Settings → Events** opens workspace-wide history. Inspect invalid values, proposed replacements, affected saved rules and producer checks there. A reviewed proposal is not an applied schema. See [Setup changes](/setup-changes) for checks, producer tests, workflow waiting, application and recovery. Address the reported blockers and use the offered application action only after the review matches your intended meaning. Some changes prepare data while the current setup stays live; the final switch can pause the workspace. During a pause, refused record writes or producer events need a retry after reopening. Already retained workflow entries are checked separately. Keep the change page open, inspect completion and any resumption warnings, and use **Check Apply status** after an uncertain application response instead of assuming the change failed. For scheduled evaluated fields, refine the definition, review the replacement and inspect the next published result after application. These fields must allow empty values and cannot also use a stored-field default or AI autofill. Changing a display name alone does not regenerate the calculation. Deletion goes through the same dependency checks. Read the named consumers and repair or remove their references before retrying. Integration-managed schemas and attributes are read-only for manual definition changes. ## Use a field's value Use the record and field paths shown in the editor's **Data** tab. A broadcast's contact key comes from its contact schema; templates and workflows can declare their own data keys. Stored writable fields arrive with the record. Evaluated and scheduled evaluated attributes require [expansion](/attribute-expansion). The public [Attributes API](/api/attributes) describes field definitions. The [Records API](/api/records) reads and writes supported stored record data; a plain record response does not include evaluated relationships or published scheduled values. See [Liquid](/liquid) for email field paths and fallbacks. --- # Sending Avatars and Inbox Branding Source: https://docs.maxclicks.ai/avatar # Sending Avatars and Inbox Branding Inbox branding helps a customer recognize the sender before opening a message. The image shown beside an email depends on the receiving provider and its supported identity signals; it is separate from the logo in your maxclicks [brand kit](/brandkits). maxclicks does not manage sending avatars. The avatar comes from the mailbox you send from (Google Workspace or Microsoft 365, for example) and from the recipient's inbox provider. Nothing on this page is set up inside maxclicks: it is background on how inboxes decide what to show. ## Where avatars come from The receiving inbox controls sender-image display. A mailbox profile picture, a brand-kit logo and a BIMI logo are different settings. Uploading an email logo in maxclicks changes the content you author; it does not configure the image beside the sender in another provider’s inbox. ## BIMI BIMI lets participating inboxes display a domain’s authenticated brand logo. Provider requirements still apply. Gmail’s setup requires a VMC or CMC certificate, an appropriate SVG logo, public HTTPS hosting and a DMARC policy of `quarantine` or `reject` applied to all messages. Follow [Google’s current BIMI setup instructions](https://knowledge.workspace.google.com/admin/security/set-up-bimi?hl=en) for the exact certificate, file and DNS requirements. Use maxclicks [domain verification](/domain) to inspect the sending-authentication records supplied for your domain. Configure the additional BIMI record and hosted certificate with your DNS and hosting providers. A verified sending domain by itself is not a promise that an inbox will show your logo. maxclicks can surface a `bimi` recommendation when the sending network reports a missing or malformed record. This is a diagnostic recommendation; it does not upload your logo, issue a certificate or change DNS for you. ## Check the result Send an actual test email and inspect it in the providers your audience uses. Check the message’s authentication results and the provider’s BIMI requirements when the logo is absent. Changing the brand kit again will not repair an external BIMI configuration. --- # How billing works Source: https://docs.maxclicks.ai/billing # How billing works Premium costs **$49 per workspace per month**, including **5,000 contact and object records**, **50,000 recipient sends** and unlimited collaborators and maxinja access. Workspace roles still control permissions. All AI usage is paid separately. There are no seat charges, monthly AI allowances or signup/domain-verification credit gifts. Each workspace has its own subscription and prepaid usage balance. Admins manage both in **Settings → Billing**. | Component | Monthly price or rate | | ------------------------------------------------------------------------------------ | ----------------------------: | | Premium base, including 5,000 records and 50,000 sends | $49 | | Each extra 1,000-record capacity pack, including 10,000 extra sends for a full month | $5 | | Email above the included allowance | $0.40 per 1,000 recipients | | AI model and metered tool usage | Metered, from prepaid credits | | Collaborators and maxinja access | No per-seat charge | These are USD prices before applicable tax. EUR and GBP use the same numeric amounts. [Your billing country determines the currency](#currency-and-tax). New subscriptions are monthly. The subscription pays for the platform and reserved record capacity. Email overage and AI use prepaid credits. Buying credits funds the wallet; using them does not charge the card a second time for the same work. [AI and usage credits](/billing/credits) explains estimates and top-ups. ## What you get on Free Free is a sandbox with **100 recipient sends each month**, a hard sending cap, and no subscription fee. Contacts and objects are not billed. It includes access to the editor, workflows, forms and maxinja within workspace permissions. Emails and forms carry the maxclicks badge. No AI credits are included. Admins can purchase credits or enable auto reload with a saved card. **Buying credits does not increase the Free sending cap.** Eligible initial maxinja setup assistance can be platform-funded; this does not make ordinary AI generation or previews free. ## What Premium costs A record is a **contact or an object**. Customers, companies, orders and products can all count. Events and logs do not count toward this record total. Premium includes 5,000 records; additional capacity is charged in 1,000-record packs, rounding a partly used pack up. ### Example: a team with 6,200 records The 1,200 records above the included 5,000 require two extra packs. | Monthly subscription | Amount | | ------------------------------ | ------------: | | Workspace base | $49 | | Two extra record packs | $10 | | **Total before tax and usage** | **$59/month** | At those quantities for a full month, the workspace includes **70,000 recipient sends**. The number of teammates does not change the bill. AI and email above 70,000 use prepaid credits. For example, another 50,000 sends cost $20 before any AI usage. ### Team access There are no paid maxinja seats. Collaborators and maxinja access are included; [member roles](/members) determine what each person can read, edit or administer. maxinja still requires write permission and sufficient funding for paid work. ### Monthly capacity changes New subscriptions are monthly. Reserved capacity grows when stored contacts and objects require more packs. Mid-month increases are charged for the remaining part of the month, and their extra email quota is prorated for the same period after successful payment. Each paid pack adds 10,000 sends for a full month. Reducing the record count can reduce capacity at the **next paid monthly renewal**. Deleting and reimporting records does not repeatedly grant email allowance. The current cycle uses paid reserved capacity, not a fresh allowance derived from the live record count. Historical annual subscriptions retain their existing renewal handling; annual billing is unavailable for new purchases. ## What credits pay for Credits cover model usage, metered third-party tools and Premium email overage. Standard internal workflow steps, maxinja exchanges, template creation and campaign dispatch have **no separate fixed platform operation fee**. Any model or paid tool work they invoke is metered separately. **Email delivery and AI writing are separate costs.** Included sends do not include AI generation. Ordinary copy and Liquid can use the email allowance without an AI charge. Broadcasts, workflows, API sends and **Send test** share the email allowance. Each To, CC and BCC recipient counts as a send. The allowance resets on the workspace's monthly cycle, not the first of the calendar month, and unused sends do not roll over. New subscriptions align this cycle with activation. Premium email beyond the current allowance costs **$0.40 per 1,000 recipients**. Free stops at 100 instead of charging overage. Included static sends do not require an AI credit balance, although sending and permission requirements still apply. ### Read the campaign estimate Open **Estimate** in the email composer before scaling a campaign. Review generation, corrections, reviews and delivery. An estimate depends on the available context and model; unknown costs are not zero. [Credits and usage](/billing/credits#estimate-a-campaign) explains what consumes credits. ## Topping up your balance In **Billing & Usage → Overview**, use the credit balance to add funds or enable automatic reload. Both are available to admins on Free and Premium with a saved payment method. [See purchase amounts and auto reload](/billing/credits#add-credits). ## Seeing where the money went **Overview** shows your plan, allowances, credit balance and invoices. **Usage** reports spending by period, category and member. Both plans have usage reporting and invoices for credit purchases; Premium also has subscription invoices. [Manage your billing](/guides/billing) explains the controls. ## When a send or an AI call is refused Check whether the work lacks **credits** or **email allowance**. Adding credits can resume work waiting for funding; it does not repair invalid content or raise Free's email cap. Avoid starting replacement work while the original waits. [What happens when credits run out](/billing/credits#when-credits-run-out) explains the distinction. ## Currency and tax Your billing country selects EUR for the EU, EEA and Switzerland, GBP for the United Kingdom, and USD elsewhere. An active subscription or a previous invoice locks that currency. Applicable tax is shown when charged. ## Cancelling Premium Choose **Cancel subscription** on the plan card. Premium stays active until the end of the period already paid for. **Resume subscription** reverses the cancellation before then. After that period, the workspace returns to Free and its 100-send monthly cap. Purchased credits carry over, and admins can still buy more. Cancelling does not disable credit auto reload or stop running automation. Review reload settings and stop or pause unwanted work separately. --- # AI and usage credits Source: https://docs.maxclicks.ai/billing/credits # AI and usage credits Credits pay for work you run in maxclicks. They are separate from your subscription, and each workspace has its own balance. **One credit equals one unit of your billing currency: $1, €1 or £1.** Small tasks use fractions of a credit. You buy credits upfront. Work then spends that balance; it is not billed to your card a second time. See [How billing works](/billing) for subscription prices and email allowances. ## What uses credits | Activity | What you pay for | | ----------------- | --------------------------------------------------------------------------------------------------------------- | | **maxinja** | Agent conversations, generation and other billable work performed for your request. | | **Adaptyle** | Generated previews, regenerations and personalized content prepared for recipients. | | **Workflows** | Model and metered tool work, plus email above the included allowance. Standard step execution has no fixed fee. | | **Email overage** | Premium delivery above the included monthly email allowance. | | **Other AI work** | Tasks such as generating an audience condition or processing brand content. | **AI previews use credits.** Reading a saved preview does not generate it again. A new generation can incur new usage. Ordinary copy and Liquid do not use Adaptyle generation credits. Paid tool calls and email overage can still apply. ## Included and purchased credits There is **no included monthly AI allowance**, no per-member balance and no credit gift on signup, domain verification or Premium activation. All teammates and background automation use the workspace's prepaid balance. Purchased credits remain available across monthly resets and when Premium ends. Eligible initial maxinja setup assistance can be platform-funded. This does not make ordinary conversations, AI previews or generation free. After setup, paid work needs available workspace credits. ## How charges are calculated AI cost varies with the model, customer context, output and any additional generation or review. There is no fixed price per personalized email. Model calls, image generation and metered third-party tools are charged by usage from prepaid credits. EUR and GBP workspaces use the same numeric credit amounts under the workspace's presentment rules. An admin can see spending by period, category and member in **Billing → Usage**. Internal platform operations have no separate fixed fee: maxinja exchanges, email template creation, campaign dispatch and workflow steps are covered by the platform. Model and paid tool calls made by those operations still consume credits. Premium email overage has its own price of **0.40 credits per 1,000 recipients**, separate from AI usage. Email within the current sending allowance has no delivery charge and does not require paid AI credit when the content does not call AI. [Workflow billing](/workflows/billing) explains how models, tools and sending affect a journey's usage. ## Estimate a campaign Before scheduling, open the email composer's **Estimate**. Review: * **Audience and model:** the number of recipient messages and the selected model affect the work. * **Generation and review:** previews, corrections, pilot reviews and continuing reviews can add usage. Do not assume one model call per recipient. * **Delivery and campaign charges:** included email delivery is separate from AI writing and dispatch costs. An unknown estimate is not a zero cost. A successful preview does not pay for later recipient generation. Stopping or cancelling a campaign does not undo usage already incurred. Use representative customer profiles, keep exact facts and fixed wording outside Adaptyle regions, and check the workspace balance before starting a large campaign. ## Add credits An admin can buy credits on **Free or Premium**: 1. Open **Settings → Billing → Overview**. 2. In **Credits Balance**, select **Add Credits**. 3. Enter a whole amount from **10 to 10,000 credits** and confirm payment. Add a card or complete your bank's verification if prompted. The purchase charges your card and adds credits to the workspace balance. It does not change your plan or increase Free's 100-email cap. ## Automatic reload In **Credits Balance**, choose **Enable Auto Reload**. Set a trigger balance and the amount to add. Both plans support it with admin access and a saved payment method. For example, set **40 credits to add** and a **20-credit trigger**. When the balance is at or below 20 at a check, maxclicks charges for 40 credits. If the balance was 15, it becomes 55. The amount is added to the balance; it is not a target ending balance. Reload checks run about every 15 minutes. They can charge a multiple of the reload amount when needed to clear the trigger. Auto reload therefore helps maintain funding, but it is not an instant refill or a spending cap. The minimum reload amount is 40 credits. The minimum trigger is the smallest 5% step of that amount worth at least 20 credits; the maximum is half the reload amount. A 40-credit reload uses a 20-credit trigger. A 1,000-credit reload has a minimum trigger of 50 credits. Use the values accepted by the controls. ## Monitor your usage Open **Billing & Usage → Usage** to filter spending by date, category and member. Admins on either plan can access it. Automated work can appear under **System**. Use **Overview** for the current balance, payment methods and invoices. Credit spending and credit purchases are different: the usage report explains the work, while an invoice records the purchase. [Manage your billing](/guides/billing) shows the controls. ## When credits run out New AI operations may be refused. A broadcast or workflow waiting for funding can continue after credits become available. Check its reason before starting replacement work, which could duplicate the original action. A failed operation is different. Adding credits does not fix invalid content, an unusable sender, a suppressed recipient or an exhausted email allowance. Free's email allowance resets monthly; buying credits does not bypass it. Premium has a limited negative-credit buffer, so **zero is not a stop button**. Use [Stop](/broadcast#cancel-or-stop-a-send) for a campaign or [Pause](/workflows/manage#pause-it-without-losing-anything) for a workflow when you intend to stop future work. For API integrations, inspect the error as well as the balance. `insufficient_credits` and `email_limit_reached` are different `402` errors. See the [error reference](/api/errors). --- # Brand kits Source: https://docs.maxclicks.ai/brandkits # Brand kits Give your agents a consistent identity to work from. A brand kit brings together your colors, typography, assets, footer and writing guidance. Publish it, then select the kit and its design choices in the emails and forms that should use them. Kits live under **Brand Kits** in the main navigation. Your business details, the ones that go in every footer, live separately in **Settings**, **Brand**. ## Start with your brand details **Settings**, then **Brand**, holds the things that are true of your business rather than of one design: | Field | What it is for | | ----------------- | ------------------------------------------------------------------ | | **Brand icon** | Small square mark. | | **Brand Logo** | Appears in emails. PNG or JPG, up to 5MB. | | **Brand Name** | Your display name in emails and footers. | | **Brand Summary** | A description of your business, used by AI when it writes for you. | | **Website** | Your main website URL. | | **Address** | Your business's postal address. | maxclicks uses this postal address in the email footer. Enter and review your business details before sending marketing email. Kit creation, publication and editing require Write permission (Member or Admin). The app’s **Settings → Brand** page is available to administrators, and saving its identity fields also requires Write permission. Non-paid Admin can inspect that page but cannot save its brand changes. Your billing address and currency are set separately, under Billing. ## Create a kit On the Brand Kits page, click **Create brand kit** (or **New brand kit** once you have one). It opens straight away with a name and description you can edit in place. A kit starts as a **Draft**, which means nothing is using it yet. A space can hold several kits, which is how you keep one look for product announcements and another for a sub-brand or a seasonal campaign. ## Fill it in A kit has five tabs. ### Content & Socials The footer that goes on the bottom of every email. * **Website**: only set this if the kit points somewhere other than your brand website. * **Copyright**: appears immediately before your brand name, so `2026 ©` renders as `2026 © Your Brand`. * **Footer** and **Disclaimers**: Markdown, for whatever else has to be down there. * **Alignment**: Left, Center or Right, applied to the whole footer including socials, disclaimers, copyright, address and the unsubscribe link. **Add Social** adds a row: pick the platform, give it a title, paste the link. Twenty-two platforms are supported: Behance, Discord, Dribbble, Facebook, GitHub, Google, Instagram, Linkedin, Medium, Pinterest, Snapchat, SoundCloud, Spotify, Telegram, TikTok, Tumblr, Vimeo, Web, WhatsApp, X, XING and YouTube. The up and down arrows on each row set the order they appear in. ### Brand Voice Your values, your tones, and two rules about what to always include and always avoid. This is what maxinja reads before it writes a word. It has [its own page](/brandvoice). ### Assets Your images, grouped into named collections. **New asset collection** starts one. An empty collection offers two ways in: **Crawl URLs**, where you paste a page address and tick the images you want off that page, or **Import manually**. Once the collection holds images, or once you have clicked **Import manually**, both ways live in the collection's **⋯** menu: **Upload file** (PNG, JPG, WEBP or GIF) and **Crawl URLs**. maxclicks looks at each image you add and writes a title and description for it, so AI can later pick the right picture for the right email. That is an AI call, so it costs a small amount of credits per image. ### Color Palettes A palette is seven named roles rather than an open list of colors, which is what lets AI apply them correctly without guessing. | Role | Where it lands | | --------------------- | ------------------------------- | | **Canvas Background** | The area behind the email body. | | **Background** | The email body itself. | | **Heading** | Heading text. | | **Text** | Body text. | | **Button Background** | Button fill. | | **Button Text** | Text on buttons. | | **Link** | Hyperlinks. | **Add color palette** creates another one, and one carries the **Default** badge. Several palettes in one kit is how you keep a light and a dark treatment on the same brand. ### Style Packs Typography and spacing: **Corners** and **Spacing** sliders, then a font family and size for **Heading** and for **Text**. Same pattern as palettes, **Add style pack** for another, one marked **Default**. ## Publish it A kit does nothing until you publish it. While it is a draft, the header tells you plainly: "This kit is a draft and is not in use yet. Publish it to use it in your emails and forms." Hit **Publish** and the badge flips to **Published**. After that, the header changes to "This kit is live. Edits apply to your emails and forms immediately." There is no second publish step or draft copy. Saved edits affect consumers that read the live kit again; they do not rewrite sent emails or an already frozen broadcast revision. Create a second kit when experimenting. **Set as default** marks the kit that gets picked automatically. Only a published kit can be the default. Publishing the first kit makes it the default when the brand does not already have one. ## Using a kit in an email An [email](/editor) saves a specific choice: which brand, which kit, and which asset collection, color palette and style pack from inside it. That is what gets handed to AI when it writes or personalizes the email, so the kit provides the design choices and writing context for that email. Review generated copy and imagery against your actual brand requirements. ## Know what changes when you edit or delete a kit Selecting a kit supplies reusable values and AI context; it does not replace every hard-coded color or sentence in an existing email. A value referenced through [Liquid](/liquid), such as `branding.colorPalette.linkColor`, follows the branding available at render time. A literal hex color or previously written paragraph stays as authored until you edit it. Broadcast preparation freezes a campaign revision for consistent rendering and sending. Do not assume that changing a live kit repairs a campaign already in preparation; check its revision and state in [Broadcasts](/broadcast). Deleting a kit affects emails and forms that selected it. If it was the default, maxclicks chooses another published kit in the same brand when one exists, otherwise clears the default. Reopen affected drafts and forms and check their kit, palette, assets, typography and footer. Kits owned by an integration cannot be deleted through the ordinary delete action. ## Related * [Brand voice](/brandvoice) * [Email editor](/editor) * [Adaptyle](/adaptyle) --- # Brand Voice Source: https://docs.maxclicks.ai/brandvoice # Brand voice Your brand voice gives agents direction for how to speak to customers: what your business values, how it sounds, and which wording to include or avoid. Define that guidance in a [brand kit](/brandkits), then review how it appears in real email samples. It lives inside a [brand kit](/brandkits): open the kit, then the **Brand Voice** tab. The card is called **Text Style**. On a kit that has no voice yet, choose **Enter manually** on that card and the Brand Values, Brand Tones and Rules sections appear. **Crawl URLs** on this voice card opens a URL or pasted-text dialog, but submitting it does not import voice. Use **Enter manually** to save values, tones and rules. The working image crawler in **Assets** is a separate action. ## Add your values **Brand Values** are what your brand stands for. Click **Add Value**, give it a name, and optionally a sentence explaining it. * `Innovative` * `Sustainability` with the description "we choose the lower-impact option even when it costs more" * `Craftsmanship` A name on its own works. A name plus one concrete sentence works better, because the description is what stops maxinja interpreting `Innovative` as "use the word innovative". ## Add your tones **Brand Tones** are how you sound. **Add Tone** takes the same name plus optional description. * `Bold` * `Warm` with "we write like a person, not a department" * `Direct` You can combine tones that pull against each other, like `playful` and `professional`, and maxinja will try to hold both. That is often exactly right for a brand that is friendly but not silly. ## Set the two rules Under **Rules** there are two free-text boxes for explicit writing instructions. They guide generation; they are not a deterministic blocked-word list or a guarantee of compliance. * **Always include**: themes to keep surfacing. The app's own example: "Mention our commitment to sustainability." * **Always avoid**: words and phrasings to keep out. The app's own example: "Avoid using jargon like 'synergy'." Write them as instructions in plain English. There is no per-word substitution list, so "never say 'cheap', say 'affordable'" belongs in Always avoid as a sentence, not as a find-and-replace pair. ## Publish the kit Voice does nothing until the kit it belongs to is published. A draft kit is not in use anywhere. Publishing makes the kit available to emails and forms. Later saved edits update the live kit without another publish step. They affect work that reads that kit again; they do not rewrite already saved prose, sent emails or frozen campaign content. Use a separate kit to test a different voice. ## Understand when voice is applied Authoring with maxinja and recipient personalization are separate moments. maxinja uses the selected brand context when it writes a draft. During an Adaptyle render, the selected kit’s values, tones and rules are included under `branding`; the prompt should tell the model how to use that guidance. A Liquid-only email does not call a writing model, so changing a tone cannot rewrite its text. A fixed paragraph also remains fixed during Adaptyle personalization. To change existing authored copy, request a draft edit and review the new copy. Try a specific rule such as “Explain the customer benefit in the first sentence. Use short sentences. Never imply a purchase we have not recorded.” Then compare samples for a customer with purchase history and one without it. ## Getting voice that actually changes the output * **Be concrete, not aspirational.** "Customer-obsessed" tells maxinja nothing it can act on. "We answer the customer's question in the first sentence, before any preamble" does. * **Keep descriptions to a sentence.** These are read alongside your prompt and your data on every write. Long essays dilute rather than sharpen. * **State exclusions explicitly.** Put banned phrases and unsupported claims in **Always avoid**, then inspect generated results. Keep exact required wording as authored copy outside [Adaptyle](/adaptyle) regions. * **Read a real result before you trust it.** Generate one template with the kit selected and read it end to end. Voice problems are obvious in a finished email and invisible in a list of adjectives. Anyone with edit access to the space can change values, tones and the rules. See [members](/members). --- # Broadcasts Source: https://docs.maxclicks.ai/broadcast # Broadcasts A broadcast brings a customer selection, an email and a send time together for one campaign. Use it for a launch, a newsletter or a reason to return. Brief maxinja on the outcome, then review the audience, message and cost before committing the send. For example, a coffee shop can announce a new roast to customers who recently bought coffee. The audience rule decides which customers qualify; the email can use each customer's purchase history to explain the offer. You choose one send time for that campaign. | Customer moment | Use | | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | | Announce the new roast to a selected group this Friday | **Broadcast**: one campaign with its own audience and email. | | Send an order confirmation when your checkout completes | **[Template](/templates)**: your application requests one saved email with that order's data. | | Wait after a purchase, check for a repeat order, then follow up | **[Workflow](/workflows)**: a triggered journey with steps and decisions. | Start with [Send a broadcast](/guides/broadcast) for a walkthrough. You need a contact schema with customer records, a [verified sender](/domain), and the appropriate [email consent](/types-emails). ## Decide who it is for Choose **New broadcast** and select the **Contact schema**. Choose a saved [segment](/segments) to narrow the audience, use **New segment** to define one, or deliberately select **All contacts in this schema**. A segment must belong to that schema. The audience page keeps the chosen schema read-only after creation. Choose **Next**, select an editor and sending mode, then **Next** to create the campaign and open **Composer**. Give it a recognizable name and description in the header, such as “New roast — recent customers”. The **Audience** tab lets you change the selection before preparation starts. maxinja can also build a condition specific to this campaign. Read its plain-language summary and check representative contacts: an audience count alone cannot tell you whether the relationship, purchase or behavior is the one you intended. The count you see while planning is an estimate of matching records. Consent, missing addresses, duplicate addresses and suppression can reduce the eventual send. The saved selection becomes fixed when campaign preparation begins; contacts who qualify afterward are not added to that run. ## Write the email Open **Composer**, or choose **Add content** if the broadcast has no email yet. The audience already determines the recipient: the email expects one contact from the selected schema. Use that contact's [data and relationships](/editor/data) for fixed personalization, and [Adaptyle](/adaptyle) when the message needs generated copy for each person. Review the sender, subject, preview text, body, links, language and unsubscribe behavior. Add valid sample data and generate a successful preview. A broadcast cannot proceed with a missing sender, empty subject or body, an incompatible recipient, or missing required preview data. Warnings such as an unnamed campaign or an intentionally broad audience are different from errors that block sending. A successful preview checks one example. It does not prove every customer's data is complete or every generated email will pass its checks. Preview examples with sparse data, different interests and different languages before scaling. ## Choose when it goes New broadcasts start with **Composer** and **Send**. The Send tab brings together the audience, content and schedule. | Choice | What happens | | ---------------------- | --------------------------------------------------------------------------------------------------- | | **Send now** | Queues preparation and sending after confirmation. It does not mean every message has already left. | | **Schedule for later** | Sets the earliest dispatch time. Preparation may begin before that time. | | **Save as draft** | Saves your work without scheduling a send. | After the first choice, the full navigation includes **Audience**, **Composer**, **Schedule**, **Emails**, **Quality review** and **Metrics**. Composer and Schedule are editable in a draft and become read-only when scheduled. A broadcast is a one-time campaign, not a recurring schedule. Check the date and time displayed in the app before confirming. The schedule is one campaign dispatch time; it does not create a separate send time for each recipient’s time zone. ### Preparation, review and dispatch maxclicks separates preparation from delivery. It fixes the audience selection and campaign revision, renders recipient messages, applies the applicable content checks, then releases eligible messages for dispatch. A scheduled campaign can start preparation early so its content is ready for the send time. Treat preparation as the point after which audience edits close, even if the scheduled delivery time is still ahead. The revision preserves the template, sender, brand materials, audience query, tracking settings and quality policy used for that run. Editing shared materials afterward does not silently replace the version being prepared. Generated content goes through the required pilot and continuing quality checks; a failed check can hold the campaign before more messages are released. See [Adaptyle](/adaptyle) for the distinction between a preview and campaign qualification. ### Check the estimate The composer's **Estimate** separates generation, possible corrections, pilot and continuing reviews, sending and campaign costs where applicable. Read the assumptions as well as the total. An unknown cost is not zero, and a duration without enough measured evidence is not a promised completion time. More recipients, richer context, different models and regeneration can change the usage. Use a representative preview to inform the estimate. Keep exact prices, dates and eligibility rules in ordinary copy or Liquid when they must remain exact for everyone. See [billing](/billing). ## Review a quality hold A quality hold protects the remaining audience while a generated email needs attention. Open **Quality review** to see rejected **Pilot review** and **Ongoing review** results for the current campaign revision. Use **Refresh reviews** to reload the list and the paging controls to inspect other reviews. **Critical failure**, **Human review needed** and **Human approved** distinguish the available action and any recorded human decision. 1. Choose **Review email** on the rejection. 2. Read the judge’s concern beside the exact rendered message. Open **Email text**, **Source information and authored instructions**, and **Approved reference before personalization** to check what the message was allowed to say. 3. If a noncritical concern is mistaken, explain why this exact email is acceptable, confirm that you personally reviewed it, and choose **Approve this reviewed email**. This requires a signed-in account with editing access and a reason of 10–4,000 characters. 4. Read the result. Approval may clear that hold, but other reviews, failures or campaign checks can still prevent sending. Approval applies only to the reviewed email and judgment in the active revision. It does not approve every future generation. Critical grounding, safety, privacy and consent failures cannot be approved here; correct the campaign for a new reviewed send. If the review is stale, reload it. A stopped or completed campaign and an email already attempted cannot receive a new approval through this action. ## Cancel or stop a send The sending banner on **Metrics** offers **Cancel send** before preparation starts. Cancellation clears that schedule and returns the broadcast to an editable state so you can revise and schedule again. Once preparation has started, the action is **Stop sending**. It cancels remaining work; messages already being dispatched may still be delivered. A stopped campaign cannot be resumed. Check its Emails tab before planning a follow-up so you do not resend to people who already received it. Stopping a send cannot recall an email or reverse rendering costs already incurred. Scheduled campaigns can begin preparing before their delivery time, so check the current banner rather than assuming the clock alone determines whether cancellation is still available. ## Who actually receives it The selection establishes who is in scope. Sending rules determine which messages may leave. * **Marketing** requires a contact subscribed to email and, if the template names a communication topic, subscribed to that topic. The email includes the required unsubscribe behavior. Any applicable channel-wide suppression blocks marketing, and a topic opt-out also blocks that address for the selected topic. * **Transactional** does not require marketing subscriptions or a topic. Delivery-failure suppressions still block addresses that hard bounced or repeatedly soft bounced. * Contacts without an address are excluded. Addresses are normalized and deduplicated within the broadcast, so two selected contacts sharing an address do not produce two messages to it. Consent and suppression remain send-time controls. The frozen audience is not permission to ignore an unsubscribe that arrives while the campaign is preparing. See [contacts](/contacts) and [email types](/types-emails). ## Watch it land **Emails** is the recipient-level record. Use it to distinguish preparation, accepted sends, failures and cancellations, then open a message to inspect its activity. An individual email’s **Sent** status means provider acceptance; **Delivered** is a later event. Neither confirms inbox placement or that a human read it. **Metrics** summarizes the campaign’s linked email records and event counts: Sent, Delivered, Opened, Clicked and Unsubscribed. Its funnel’s Sent total includes email records still pending or marked rejected/failed; it is not a count of confirmed provider acceptances. Opened and Clicked each count emails with at least one recorded event. Rates use different denominators: the **Messages clicked** card shows click-to-open, while the workspace overview shows click rate. Use [report definitions](/guides/reports#read-the-rate-and-its-denominator) for the formulas. A send failure reason appears on the same tab. Investigate a small affected sample before deciding to send again. | What interrupted the campaign | What it means for your next decision | | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Insufficient credits | Preparation or dispatch can wait for available credits. Review the wallet and usage; do not create a duplicate campaign just because progress paused. | | Quality checks | Open **Quality review** and inspect the exact rejected email and its source. Only an eligible noncritical rejection can receive human approval; other holds still need attention. | | Provider rejection or invalid recipient data | Inspect the affected email and its reason. A temporary, safely retryable provider rejection differs from a permanent rejection. | | Unknown provider acceptance | The system avoids blindly sending the message again. Reconcile its delivery before arranging another send. | | Stopped campaign | Remaining messages are cancelled. There is no general resume for a stopped send. | ## Retry eligible failed recipients A finished campaign can still have remaining work. In **Metrics**, the banner shows the remaining-recipient count. When eligible failed rows exist, a member with Write permission sees **Retry** with the number of eligible recipients. Confirm **Retry recipients** to queue those rows, then watch **Emails** and **Metrics** again. This retries failed rows in the active revision that have no attempted send or confirmed provider message. It does not repeat sent or unconfirmed messages, cancelled rows, or recipients already excluded during preparation. Rows with an existing rendered email return to dispatch; rows without one return to preparation. Quality, consent, suppression and capacity checks still apply. The count queued can change if another action changed the campaign while you reviewed it. Choose **Close remaining work** when you intend to end that unfinished work instead. This closes the remaining send permanently while preserving sent messages and their delivery records. A stopped campaign cannot use the retry action. Duplicating a campaign creates a new delivery intent rather than retrying only the remaining rows. A retry or recovery is not permission to repeat every recipient. Previously accepted messages and attempts whose acceptance is uncertain are protected from automatic redrive. ## Read an individual email’s status A status summarizes the current recorded outcome. Open the email’s **Activity** for its history; the status is not a mutually exclusive event bucket or simply the latest event. Rejection and sending errors take priority, followed by bounce, complaint, click, open, delay, delivery and sent evidence. A delayed email can therefore still have delivery activity in its history. | Status | Interpretation and next action | | --------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | Pending | The email has not recorded a send or a more specific outcome. Check campaign preparation and holds. | | Unconfirmed | A provider attempt was made without confirmed acceptance. Reconcile that attempt before arranging another send. | | Rejected or Failed | Read the rejection or sending reason on the email; a new campaign is not a retry of only this row. | | Sent | Provider acceptance is recorded; delivery feedback is separate. | | Delayed | A delivery-delay event was received. Inspect its diagnostic and retry information. | | Delivered | Delivery feedback was received; inbox placement is not established. | | Opened or Clicked | At least one corresponding event was recorded. Inspect activity and the destination before interpreting it as a human action. | | Bounced or Complained | Delivery or spam feedback needs investigation; check the resulting address suppression and consent. | A row showing **Sent · details removed**, **Failed · details removed** or **Cancelled · details removed** preserves an outcome after its private details were removed. It does not offer the former message to inspect. The absence of details is not a reason to send it again. ## Reuse or remove a campaign Use the row menu on **Broadcasts** to **Duplicate** a campaign. The copy keeps its audience configuration and email content but starts unscheduled, with new sending history. Rename it and review the current audience, dates, links and sample before sending. Duplication does not exclude people who received the original. **Delete** removes the campaign’s configuration, recipient-run associations and campaign reporting. Sent emails and their delivery history are retained. Deletion is permanent and cannot recall messages. The server refuses deletion while the campaign is actively sending or its current work is still held by a worker. Cancel or stop the send and wait for it to settle first. After confirmation, **Deletion queued** means cleanup was accepted, not completed. **Deletion is waiting** means unresolved work still needs reconciliation. The workspace remains closed to ordinary runtime work during cleanup; follow the displayed message until deletion completes before starting another operation. ## Related * [Send a broadcast](/guides/broadcast): a practical review sequence. * [Sending to large audiences](/large-audience): selection, preparation and scale. * [Email editor](/editor): content and preview controls. * [Segments](/segments): reusable audience conditions. * [Broadcasts API reference](/api/broadcasts): the public API contract. --- # Contacts Source: https://docs.maxclicks.ai/contacts # Contacts A contact represents a person you may want to reach: a customer, subscriber, student or product user. It holds their identity, email address, preferences and personal details. Contacts belong to a [schema](/schemas), which defines the additional fields your business needs. Keep the person separate from the things they use or buy. Maya is a contact; her company's account and her projects are [objects](/objects). Sarah is a contact; her four orders are four objects. An [evaluated relationship](/evaluated-attributes) lets each person's audience rule or email use the right account or orders without turning those things into recipients. A contact record does not by itself establish permission to email. Identity answers **who this is**; a segment answers **whether they fit your campaign**; subscriptions, topics and suppressions determine **whether the message is eligible to send**. You find them under **Records** in the top navigation, one page per contact schema. ## Add contacts **One at a time.** On the schema's records page, the **Add** button (labelled with your schema's name, so **Add Student** on a `Student` schema) opens a form with the built-in fields, your own [attributes](/attributes), tags, and the communication topics to put them on. **In bulk, from a CSV.** The **Import** button on the same page takes you there. See below. **From your product.** Forms, workflows and the API all write contacts. See the [Records API](/api/records). Consent is not implied by any of these. A contact created through the API with no `subscriptions` value is subscribed to email, so if you are loading people who have not opted in, send `subscriptions` as an empty list. The **Add** form in the app is the opposite: it only subscribes the contact to email if you put them on at least one topic. ## Import a CSV Open **Settings**, then **Schemas**, open the schema and go to the **Import** tab. **Download sample CSV** gives you a file with exactly the right column headers. The standard importer matches headers to writable attribute keys after trimming whitespace. It does not provide a column-mapping screen. Unknown columns, evaluated attributes and scheduled evaluated attributes are omitted. The import tracks skipped fields internally, but the current table shows imported and failed rows rather than a per-column warning. Inspect a few imported profiles to confirm the fields arrived; “Succeeded” alone does not prove your headings matched. In onboarding, maxinja can instead inspect a CSV sample, propose a mapping and start that mapped import after you review the plan. A sample preview is not validation of every row. See [Import a contact list](/guides/import). Before you upload you can set **Tags** that are added to every contact in the file, and tick **Subscribe to email by default**. Only tick **Subscribe to email by default** if you have explicit consent to email everyone in the file. It fills the blanks and nothing more: a row carrying its own `subscriptions` value keeps that value, so consent can vary inside one file. The default reaches only the contacts this import creates, never one it matches to a contact you already have, whose subscriptions stay as they are unless the row says otherwise. Leave the box unticked and a created contact whose `subscriptions` cell is blank ends up subscribed to nothing. A file can be up to 50 MB and 500,000 rows. The importer trims cell text and converts supported number, boolean and multi-value fields before validating records. Use `true` and `false` for booleans: `true`, `yes`, `1` and `on` are interpreted as true; other nonempty text becomes false, so a misspelling may not fail the row. Blank cells are omitted. On an existing contact, omission leaves the old value in place; it does not clear it or rerun creation-time autofill. On a new contact, the field's missing-value policy applies. Check **Pending**, **Running**, **Succeeded**, **Partial**, **Failed** or **Canceled** status, then review failure samples and the downloadable log. Canceling does not undo rows already imported. ## Consent: who you are allowed to email Marketing email checks the contact's channel subscription and, when selected for the email, topic membership. These are separate from identity, tags and segment membership. Adding a tag such as `newsletter` does not by itself subscribe the contact to a communication topic. **Channel** is how you reach them. Email is the only one today. **Topic** is a category you define, like `Product updates` or `Weekly digest`, under **Settings**, then **Topics**. Each topic is public or private. Public topics appear on the preferences page so contacts manage them themselves. A private topic stays hidden from anyone you have not put on it. On the records table you can select contacts and use **Subscribe**, **Unsubscribe**, **Add topics** and **Remove topics** to change this in bulk. On a single contact's page, **Communication topics** does the same thing. ### The page your contacts see Every marketing email carries an unsubscribe link to a preferences page maxclicks hosts for you. The link is signed, so the contact never signs in. There they can turn topics on and off, **Save preferences**, or **Unsubscribe from all future emails**. A private topic only appears for a contact you have already put on it, and that contact can turn it off from the same page. ### Double opt-in Turn on **Enable Double Opt-In** on a [form](/forms) to hold opted-in submissions for confirmation. maxclicks emails a single-use link; clicking it creates the contact or updates the matched contact. The link expires 24 hours after submission, after which the person must submit again. If an optional consent checkbox is left unchecked, no confirmation email is sent and the details can be saved immediately. A new contact remains unsubscribed; a matched contact keeps its existing subscriptions and topics. The hosted form still shows its check-email screen in this case, so test both consent choices. ### The consent record maxclicks keeps for you Every consent change is logged automatically: subscribes, unsubscribes, topic changes, suppressions, double opt-in confirmations. Each entry keeps what happened, the resulting state, whether it was single or double opt-in, who did it, and the surrounding context including the consent text, source, form and timestamp. The IP address and user agent are recorded with the entry, but they are stripped from the context before it is returned, so the API never hands them back. You do not have to do anything to get this, and it is what you reach for when someone asks you to prove a person opted in. The trail is read through the [Records API](/api/records) rather than shown in the app. ## Suppressions: the do-not-contact list A suppression is a mark on an email address, not on a contact. It covers every contact that shares that address, including ones you create later. Addresses are stored hashed. | Reason | Where it comes from | | ------------------------ | ------------------------------------------------------------------------------------------- | | Unsubscribed | An unsubscribe action | | Hard bounce | Email delivery feedback | | Consecutive soft bounces | A supported sending block; current delivery-event handling does not create it automatically | | Spam complaint | Complaint feedback | | Manual or Legal | A workspace action with a selected reason and optional details | On a contact's page, **Suppress email** adds a Manual or Legal suppression. **Remove** appears for workspace members with write access; removal is also subject to the operation's scope and allowed reasons. The public API has its own removal restrictions, so use its reference rather than assuming every visible UI action is available to an API key. Marketing email is blocked by any applicable suppression. Transactional email has different consent rules but still checks bounce suppressions. A workspace-scoped suppression affects that workspace, while a platform-scoped suppression can apply more broadly. A topic opt-out also creates an address-and-topic block in that workspace. It applies to other contacts sharing the address for marketing email assigned to that topic, including later profiles. It does not block every topic or transactional email. This address-level block is distinct from the channel suppression list and from one profile's topic membership. Removing a channel suppression does not itself grant a missing channel subscription or topic membership, or remove a separate topic block. Inspect the complete preference state before expecting the next send to qualify. You can suppress a phone number too, and it is recorded, but nothing acts on it yet: email is the only channel maxclicks sends on. ## How maxclicks knows two records are the same person When a contact arrives, maxclicks looks inside that schema for someone it already has, checking in this order: 1. The maxclicks id, when the write carries one. 2. `userId`, your own identifier for the person. 3. `email`, when there is no `userId`. 4. `phone`, when there is neither. A consistent identifier lets an upsert or import update the same contact. This does not automatically merge profiles: an email-only record and a record with a new `userId` can represent the same human separately. Inside a schema, uniqueness follows the identity priority above; use one stable source identifier and inspect conflicts when changing identity strategy. ### When two contacts want the same address If a write would hand a contact an identifier that already belongs to somebody else, three routes carry on rather than throwing the whole write away: the upsert endpoint (`POST /v1/schemas/{schema}/records/upsert`), form submissions and workflow steps. They keep the old identifier, apply everything else you sent, and report the change they had to skip. They try your new values first and fall back to the stored ones until they find a combination nobody else holds. Everywhere else the conflicting write is not applied at all. Creating a record (`POST /v1/schemas/{schema}/records`) and patching one (`PATCH /v1/schemas/{schema}/records/{id}`) both answer with a conflict error and write nothing, and editing the identifier in the app fails the same way. A clashing CSV row is not half-applied either: it is counted as a failed row, with "Conflicting identifiers (email/phone/userId)." against it in the import's failure log, while the rest of the file goes in as usual. Resolve the source identity before retrying. Compare the two records and decide which identifier belongs to which person; update your upstream mapping and correct the affected records. Do not assume maxclicks will merge profiles, consent or history for you. Anything you send is saved, not just the identifiers. Sending an email with `{ email, firstName }` updates that contact's first name on the way through. ## The fields every contact has Plus your own [attributes](/attributes). | Field | Key | Notes | | ---------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------- | | Full Name | `fullName` | Built from the first and last name | | First Name, Last Name | `firstName`, `lastName` | | | Email Address | `email` | | | Phone Number | `phone` | | | User ID | `userId` | Your own identifier for the person | | Avatar URL | `avatarUrl` | | | Notes | `notes` | | | Tags | `tags` | | | Subscriptions | `subscriptions` | The channels this person has consented to | | Topic IDs | `topicIds` | Setting this replaces their current topics rather than adding to them | | Source | `source` | Import, API, form, manual, workflow or integration. Set by maxclicks, and you cannot write it | | Email Statistics | `emailStatistics` | Sent, delivered, bounced, complained, unsubscribed, unique opens, unique clicks. Set by maxclicks, and you cannot write it | | Created At, Updated At | `createdAt`, `updatedAt` | | Setting one of your own fields to nothing clears its value but keeps the field on the record. ## Working through the table Select rows and maxclicks offers **Subscribe**, **Unsubscribe**, **Add tags**, **Add topics**, **Remove topics** and **Delete**. Search filters the list, and **Select Columns** decides which fields you see, including your own. The contact list orders newest records first, with a stable ID tie-break. The first page refreshes the count; subsequent pages reuse that count. Search covers identity text, notes and tags rather than acting as an arbitrary filter over every custom field. Use a [segment](/segments) for a business rule such as renewal date or activity threshold. Deleting contacts is permanent and asks you to confirm first. Cleanup is an operation that can continue in the background, including associated events and calculated data. The workspace can temporarily reject other processing while that cleanup is active. Wait for the deletion status to complete; the initial acceptance is not proof that every related store has been cleared. A record opened during review can also contain evaluated relationships and scheduled evaluated attributes. Use **Compute value** to inspect the field; for a scheduled evaluated attribute this reads a published result, not a fresh event calculation. Check [publication status](/attributes#computed-attributes) when freshness matters. Open a contact for their **Profile**, an **Activities** timeline (created, updated, suppressed) and **Analytics**. ## Related * [Schemas](/schemas): defining the shape a contact takes. * [Segments](/segments): describe an audience and email it. * [Forms](/forms): collect contacts from your site, with consent. * [Records API](/api/records) and [Suppressions API](/api/suppressions): doing all of this from your own code. --- # maxclicks developer portal Source: https://docs.maxclicks.ai/developers # maxclicks developer portal Build customer engagement into your product with the maxclicks Public API: keep customer records current, send events, send templates your team prepared, and read campaign results. Every tool below speaks the same REST API at `https://api.maxclicks.ai/v1`. ## Get credentials Create an API key in the app under **Settings**, then **Developers**, on the **API keys** tab ([open it](https://app.maxclicks.ai/space/-/settings/developers)). A key acts for one workspace. Apps that act for a user connect with OAuth instead, so the user chooses the workspace and permissions. [Authentication](/api/authentication) covers both. Confirm the key before writing anything: ```bash curl --fail-with-body https://api.maxclicks.ai/v1/me \ -H "Authorization: Bearer $MAXCLICKS_API_KEY" ``` ## Start building | Goal | Start here | | ------------------------------------------ | ---------------------------------------------------------------------------------------- | | Sync a contact and send a prepared email | [Developer quickstart](/quickstart) | | Look up an endpoint, its fields and errors | [API reference](/api) | | Generate a client or validate requests | [OpenAPI specification (JSON)](/api-spec/openapi.json) or [YAML](/api-spec/openapi.yaml) | | Use a typed client library | [SDKs](/sdks) for Node.js, Python, PHP, Ruby, Go, Java, Rust and Elixir | | Script from a terminal or a coding agent | [maxclicks CLI](/ai/cli) | | Give an assistant tools through MCP | [MCP server](/ai/mcp) | | Give a coding agent the platform context | [Agent Skills](/ai/skills) | | Receive record changes and email activity | [Webhooks](/webhooks) | | Retry writes and sends safely | [Idempotency](/api/idempotency) | ## Test safely Create a separate workspace for development and give it its own API key, so test records and sends stay apart from your customers. Sends from any workspace reach real recipients, so use addresses you control. Event collection sources also have a **Test** environment for browser and server events. ## Limits and stability * **Rate limits.** Rate-limited responses carry `RateLimit-Policy` and `RateLimit` headers, and a `429` adds `Retry-After`. [Rate limits](/api/authentication#rate-limits) lists the budgets. * **Versioning.** The API is versioned in its path. [Versioning and deprecation](/api#versioning) explains what can change within `/v1` and how a deprecation is announced. ## For agents * [llms.txt](/llms.txt) indexes these docs, and [llms-full.txt](/llms-full.txt) contains them in full. * Request any page with `Accept: text/markdown` to receive it as Markdown. * The MCP server card is at [`/.well-known/mcp/server-card.json`](/.well-known/mcp/server-card.json). --- # Domains Source: https://docs.maxclicks.ai/domain # Domains Send from an identity your customers recognize and your business controls. Add a domain such as `mail.example.com`, publish the supplied DNS records and complete verification before using it for a campaign, template or workflow email. Until a domain is verified, nothing sends from it. Broadcasts and template sends refuse with "domain not verified", and so do test sends from the editor: a test goes out through your own verified domain and picked sender, exactly like a real send. Verify a domain before you plan to preview anything in a real inbox. Domains live in **Settings**, then **Domains** under Workspace settings. The page is called **Sending Domains**. For example, verifying `mail.example.com` lets you configure a sender such as `Halden `. The domain establishes the authorized sending identity; the sender profile supplies the address and display name. You need editing access in maxclicks and access to the DNS provider that hosts the domain’s records. ## Add your sending domain ### Add the domain Click **Add domain** and type the name. maxclicks checks it as you type and tells you if it has already been added. The name cannot be changed afterwards, so if you want to send from a subdomain (and you usually do, see [subdomains vs root domains](/subdomain)) decide that now. ### Copy the DNS records into your registrar Verification opens straight after you add the domain, showing every record you need. Each **Name** and **Value** has a copy button next to it. Paste them into your DNS provider exactly as shown. ### Start the check Click **Start verification check**. maxclicks then keeps checking on its own through the day and flips the domain to verified as soon as your records resolve. DNS caches and provider checks can delay verification. A Pending result immediately after a change is not proof the record is wrong; compare the exact record names and values, then check again after the provider has published them. ## The records you add maxclicks generates all of them at once. You get one or more DKIM records, plus SPF, bounce handling and DMARC. | Type | What it is for | | ---------------------------------------------- | -------------------------------------------------------------------------------- | | `CNAME` (one or more, on a `_domainkey` name) | DKIM. Signs your mail so mailbox providers can prove it really came from you. | | `TXT` on the return-path subdomain | SPF. Authorizes maxclicks to send on your behalf. | | `MX` on the return-path subdomain, priority 10 | Bounce handling. Routes the delivery failures for your mail back to maxclicks. | | `TXT` on `_dmarc` | DMARC. Tells mailbox providers what to do with mail that fails the checks above. | The return-path is a label under your sending domain, commonly `send.`. Use the exact name generated for your domain rather than reconstructing it. It is the bounce-handling address, distinct from the visible From address. Each record carries a status you can watch: **Not Started**, **Pending**, **Verified**, **Failed** or **Temporary Failure**. The whole domain shows **Verified For Sending** or **Not Verified For Sending** at the top. Leave every record in place for as long as you send. maxclicks re-checks through the day, and if the records disappear it un-verifies the domain and all sending from it stops. ## Diagnose a blocked send Read the overall **Verified For Sending** status, then the individual authentication records. Copying a record into a DNS dashboard is only the first step; maxclicks must observe the required state before the domain can send. Verification and permission to send are separate checks. A verified domain can still belong to a workspace whose sending is disabled because of delivery or reputation issues. Inspect the current message and the workspace Inbox instead of repeatedly recreating the domain. A successful in-app preview needs no inbox delivery; **Send test** does, and uses the same verified domain requirement as other sends. ### If a record stays unverified Compare the record’s type, full DNS name and value with the verification dialog. Some DNS providers append your domain automatically; check the resulting name so it does not contain the domain twice. Use the supplied return-path name for SPF and MX, and keep the `_domainkey` portion of DKIM names intact. Change the record that failed, then inspect its status again. Keep existing DNS records used by your website and business mail. If a generated name already has a record or your provider rejects the entry, have the person who manages your DNS reconcile it instead of replacing unrelated settings. ## The default domain The first domain you add becomes the space default, used wherever you do not pick one yourself. It becomes the default when you add it, not when it verifies: verification is what lets it send. **Set as default** on any row moves it, but only a verified domain can hold the spot. The menu item stays clickable on an unverified domain, and running it there is refused: you get a "Failed to set domain as default" message and nothing changes. Verify the domain first. Moving the default also moves the default sender. It becomes the first sender profile saved on the new default domain, or nothing at all when that domain has no profiles yet. If you delete the default, maxclicks picks another verified domain to replace it, or leaves the space without a default when none is verified. Deleting a domain takes more with it than the domain itself. Every sender profile saved on it is deleted too, and any broadcast, template or workflow email whose from-address pointed at that domain or one of its profiles is left without one, so it refuses to send until someone picks a new from-address. Review the affected drafts and live sending contexts before confirming. ### Follow deletion through completion Domain removal is asynchronous. **Deletion queued** means the cleanup request was accepted; **Deletion is waiting** means unresolved work or an external-resource operation still needs reconciliation. The workspace remains closed during this cleanup. Follow its displayed message and wait for completion before making another sending change. Adding a replacement domain is a separate task: verify it, select replacement senders in affected content and review an inbox test before retiring the old identity. Deleting the old domain does not migrate explicit sender selections or recall existing mail. ## Sender profiles A sender profile is a saved from-address, so nobody has to retype `support@` every time they build an email. They live in **Settings**, then **Sender Profiles**. Each profile has a **Name** (the display name recipients see, like `Janet Vargas`), an optional description, and an **Email**: the part before the @ plus one of your domains. maxclicks previews the finished address as you type. Typing a name suggests a local part for you. You can save a sender before its domain verifies, but two senders in a space cannot end up at the same address. One profile is the space default. **Set as default** is only available on a profile whose domain is itself the default domain, so if the option is greyed out, set that domain as default first. ## Checking verification from your own code If a deploy script or health check needs to confirm a domain is ready before it points sending at it, the API can read domains and senders. See the [domains](/api/domains) and [senders](/api/senders) API reference. The public API reads domain and sender configuration. Domain setup and verification are managed through the app and authorized product actions. Creating a domain requires Write permission, available to Member and Admin. Non-paid Admin can inspect configuration but cannot perform that write action. A domain name already assigned to another workspace cannot be claimed again. --- # Email editor Source: https://docs.maxclicks.ai/editor # Email editor The composer is where a campaign brief becomes an email your team can inspect. Bring together the message, layout, sender, customer data and brand kit, whether the email belongs to a [broadcast](/broadcast), a [template](/templates) or a [workflow](/workflows) step. ## Build a first message For a product announcement, start with a subject, a short explanation and one button to the product page. Select your verified **From** sender and published brand kit, then inspect **Data** for the recipient key. Add a greeting with a fallback, preview two customers and use **Send test** to inspect the result in an inbox. [Design an email](/guides/design) walks through this job; the sections below explain each control. ## Choose an editor before you write Templates offer four editor starting points. A new broadcast asks for its audience first, then offers the same editor and sending-mode choices before opening **Composer**. An existing broadcast without email content offers **Add content**. | Editor | Good for | | ----------------------------------- | ------------------------------------------------------------------------------------------ | | [Dynamic Composer](/editor/dynamic) | Multi-column responsive builder. For designed emails with responsive sections and columns. | | [Plain Composer](/editor/plain) | A simpler HTML layout for a direct, written message. | | [MJML Code](/editor/mjml-code) | Write MJML directly. Responsive layout, full control. | | [HTML Code](/editor/html-code) | Write raw HTML directly. Total control, you own the responsiveness. | Your choice sticks for the life of that email. **Switch editor** offers a few one-way moves out of the visual builders, but nothing ever comes back, and a code editor never becomes visual again. If you want a different starting place, start a new template or broadcast. ## Find the control you need The panel beside the email has four tabs. On narrow screens it collapses to icons; select a tab to open it over the canvas. | Tab | Use it to | | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | **Design** (**Outline** in code editors) | Change the selected block, subject, preview text or email setup. The selection trail tells you what you are editing. | | **Personalize** | Review Adaptyle prompts and variables, select runtime AI settings and read the cost estimate. | | **Data** | Declare the values the email needs, choose its recipient and inspect sample data. | | **Checks** | Find errors and warnings that affect readiness. | The maxinja input at the bottom of the panel follows that context. “Make this shorter” on a selected subject is a subject edit; the same request on the whole email is a broader brief. Read the scope label before sending your request. Changes are saved to the current email, so review the result before scheduling or sending. ## Say whether it is marketing or transactional A template’s creation flow lets you choose its sending mode. For an existing broadcast or workflow email, check the mode configured on its content. The mode is fixed when that email is created and cannot be changed afterwards. * **Marketing** reaches only contacts who subscribed, and maxclicks manages the unsubscribe link for you. * **Transactional** does not require a marketing subscription and carries no marketing unsubscribe link. Sending still checks suppression, the sender, available capacity and account status. Use it for service messages such as a receipt or a password reset. This lives on the content, not on the send. However you send it later, an email written as marketing stays marketing. Marketing email can only go to a contact. A template addressed to a bare email address has to be transactional. ## Set who it comes from **From** sits at the top of the editor and is required. Either pick one of your saved senders, or pick a verified [domain](/domain) and type the part that goes before the `@` plus an optional display name. New domain choices must be verified for sending. An already selected domain can remain visible with an unverified warning so you can repair it; its presence does not mean it can send. ### Route replies and copies In email-level **Design → Sender** (or **Outline** in code editors), expand **Reply to, Cc and Bcc**. Enter comma-separated addresses, optionally with display names, and press Enter or leave the field to save them. **Reply to** directs replies to a different address. **Cc** and **Bcc** receive copies of the same personalized email; Cc is visible to other recipients, while Bcc is hidden. They are fixed addresses on this email configuration, so a broadcast can send them a copy for every audience recipient. Review that choice before a large send. Copies consume recipient allowance, and a suppressed envelope address can block the attempt. They are not additional contacts used to personalize the message. ## Tell it what the email needs, and who it goes to Open the **Data** tab beside the email. Two things live there: * The list of information the email is allowed to reference: a contact, an order, a plain value you pass in. Anything not on the list cannot be used in the content. * **To**, the one entry the email is addressed to. Only a single contact record or an email-address value can be picked. The same tab holds the [sample data](/editor/sample-data) the preview renders against. The available controls depend on whether you opened a template, broadcast or workflow email. [Data and recipient](/editor/data) covers it in full: adding and removing properties, the naming rules, why some recipients are not offered, and what a broadcast locks that a template does not. ## Bring in your brand Email setup in **Design** lets you select a published [brand kit](/brandkits), an asset collection, a color palette and a style pack. Only published kits appear. Your choice arrives in the email as `branding`, so you write `{{ branding.logoUrl }}` with [Liquid](/liquid) instead of pasting colors and logo links by hand. References resolve from the branding used for that render; already sent or frozen campaign content keeps its existing values. ## Personalize it Two tools, available in all four editors and in the subject line as well as the body: * **[Liquid](/liquid)** drops in a value maxclicks already has, like a first name, and adds simple logic. * **[Adaptyle](/adaptyle)** takes an instruction in plain English and uses the email’s selected runtime model to write that passage for each recipient. ## Tag links for your analytics Email-level **Link tracking** uses the workspace’s UTM settings for marketing email. Set **Campaign** to a name you recognize in your analytics, such as `new-roast-september`; leaving it blank uses a normalized version of the broadcast, template or workflow name. Source and medium normally come from **Settings → Link tracking**. At rendering, maxclicks adds the configured `utm_campaign`, `utm_source` and `utm_medium` values to eligible absolute HTTP or HTTPS links, replacing those same parameters when already present. Platform links such as preferences are excluded. Transactional email is not tagged by this feature. UTM attribution in your website analytics is separate from the [email delivery and engagement reports](/guides/reports). ## Preview it before you send The **Preview** button renders the email against your sample data. If the email has no Adaptyle instructions in it, the Base preview resolves it without a model call. If it does, the panel gives you two views: **Base**, the email as written with the instructions stripped out, and **Personalized**, which stays empty until you press **Generate preview**. **Generate preview** requests personalized rendering and spends credits when a prompt reaches the model. maxinja can also generate samples as part of an authoring task. It also needs a model and a thinking depth configured under **Personalize → Runs with**, and it will not run while the email still has blocking errors. **Send test** sends the current rendered sample to the address you enter. It prefers the generated personalized sample when one exists, otherwise the Base sample; switching the displayed tab to Base does not change that priority. The test does not apply this template’s Cc or Bcc configuration. Render a preview first; then use a complete sender on a verified [domain](/domain). The server also checks edit permission, email allowance, sending status and recipient suppression. A test is a real transactional email and uses your email allowance. It has no contact association and does not start your workspace’s email-event workflows or webhooks through the test-send action. It verifies the rendered content and inbox appearance; it does not verify the production recipient mapping, marketing consent or audience selection. If its outcome is unknown, check email history before attempting another send. ## Check it is ready Open **Checks** in the right panel to see **Ready to send**, an issue count or a warning count. Issues are grouped under Sender, Recipient, Data, Content, Brand kit, Topic and Sample & preview, so you can identify what needs attention. The editor’s readiness check requires a sender, a valid recipient, a subject, a body and a preview that rendered without errors. When the content carries Adaptyle instructions, the model and thinking depth under **Personalize** have to be set as well, because that pair is what writes the content on every send. There is no default behind them. ## Read the estimate before scaling up Under **Personalize**, the **Estimate** section separates generation, possible corrections, pilot reviews, rolling reviews, sending and campaign operation costs. A broadcast uses the current audience count; other editor surfaces let you choose an assumed audience size. * **Not fully estimated** means at least one cost is unknown. The known partial subtotal is not a complete campaign budget. * Preview generation scenarios use the selected model and sample. They are not guaranteed price bounds or an average across the audience, and are not added into the campaign total. * **Recipient-dependent** means a Liquid condition may leave AI instructions for some recipients and remove them for others. One static preview does not establish that the campaign has no AI usage. * **Completion time: Not calibrated** is not a delivery promise. Available daily capacity is shared and checked again before sending. * A quality-review warning means the draft is not yet cleared for AI campaign delivery. Open **How this estimate works** for the assumptions behind the figures. Use [Billing usage](/billing) for actual charges. ## Moving to code later **Switch editor** offers the one-way moves, and it only appears in the two visual builders. From the Dynamic Composer you get **Code Editor**, which writes your layout out as MJML source, and **Simple HTML**, which flattens sections and columns into the Plain Composer's block list and discards your preview text along with every link set on an image. From the Plain Composer you get **Code Editor**, which writes your blocks out as HTML. maxclicks asks you to confirm each of these, because none of them can be undone. The editor’s maxinja input can draft or refine the current email. Select a subject, section or block to give the request that context, then describe the change. This edits the saved draft. An [Adaptyle](/adaptyle) instruction has a different job: it stays in the template and runs when recipient content is rendered. ## Sending what you built Author content in the app, manually or with maxinja. The [Public API](/api) sends an existing template with data; it does not expose subject/body authoring. See [templates](/templates) for application-triggered messages and [broadcasts](/broadcast) for a one-time audience send. Editor readiness checks the draft and sample. Public template sending validates its actual inputs at execution time; it does not require the editor’s saved sample preview. Execution still checks current eligibility, available credits and sending capacity. An AI broadcast must also satisfy its campaign [preparation and quality checks](/broadcast). A successful preview is not approval to bypass those checks. ## Related * [Data and recipient](/editor/data): what the email is allowed to reference, who it goes to, and what each surface locks. * [Sample data](/editor/sample-data): the stand-in record the preview renders against. * [Fonts](/editor/font): what the font picker offers and what your reader actually sees. * [Adaptyle](/adaptyle) and [Liquid](/liquid): personalization. --- # Data and recipient Source: https://docs.maxclicks.ai/editor/data # Data and recipient The Data panel defines the customer context an email can use and the recipient it goes to. A purchase follow-up may need both a contact and an order; selecting those inputs makes the relationship explicit for Liquid, Adaptyle and the system that triggers the send. Open the **Data** tab beside the email. It shows the expected properties and recipient, followed by a **Sample** section. The sample’s **Edit as JSON** control opens the raw values; **Hide JSON** returns to the record pickers. The list is a contract. The sending context supplies its expected properties, while maxclicks adds reserved branding data. Each record input needs the identity and fields appropriate to that operation; including a second record does not automatically look it up through the first. On narrow windows the tabs collapse to icons. Select the Data icon to open the panel over the email, then close it to return to the full canvas. ## Example: a confirmation for one customer and order For a standalone order confirmation, declare a customer contact and an order object. Select the customer row under **To**. If you name the rows `customer` and `order`, the content can use `{{ customer.firstName | default: "there" }}` and the fields your order schema defines, such as `{{ order.total }}`. Pick a real customer and order in **Sample** and preview the confirmation. The application must provide both inputs for a real send; choosing the customer alone does not find an unrelated order. For a broadcast, the audience provides the one contact input, so reach their purchases through a selected [relationship](/evaluated-attributes) instead. ## Tell the email what it needs Each row on the list is one piece of information, with a key you use in your content: `{{ contact.firstName }}`, `{{ order.total }}`. There are two kinds. | Kind | What it is | How the row reads | | ------ | --------------------------------------------------------------------- | ------------------------------------------------ | | Record | A [contact, object, or event](/schemas) record, one or a list of them | The schema's name, with `[]` after it for a list | | JSON | Any plain value you pass in: a coupon code, a total, a nested object | `JSON` | Click a row to open it. A record shows every field of its schema. A JSON row shows the shape you defined for it. The first row is always `branding`, ticked and greyed out, with a note reading "The branding data is added by the system. You do not need to set it in the sample data." That is your [brand kit](/brandkits), added for you. A reference to something that is not on the list renders as nothing at all. Every editor warns you before that happens: in the visual editors the variable turns amber, and in the MJML and HTML code editors the expression is underlined. Both carry the same message, `"name" isn't in your data, so this renders empty.` ## Add a property Press **Add property**, beside the sentence above the list. It is there on a [template](/templates) and on a workflow send-email step, and never on a [broadcast](/broadcast). 1. **Property type**, placeholder "Pick a schema or JSON…". The dropdown groups your schemas under **Contact schemas**, **Object schemas**, and **Event schemas**, then offers **JSON property** under **Other**. Every schema in your space is offered, and the same schema can appear on the list more than once under different keys. 2. **Key**. It is pre-filled from the schema, and you can type your own. 3. **Array of records**, for a record. Off means one record, on means a list. 4. **Shape**, for a JSON property. Paste a sample payload and maxclicks works the shape out, build it by hand, or leave it open to accept anything. 5. **Add**. Changing **Property type** after you have typed a key throws the key away and starts the row over. Pick the type first, then name it. ### Naming rules * In **Add property**, use letters, digits and underscores, starting with a letter or underscore. Spaces become underscores and other symbols are removed when the property is added. The panel previews the result as "Will be normalized to `yourkey`". A digit cannot be the first character. * `branding`, `skip`, and `limit` are taken. Using one gives you "Reserved." Expression keywords such as `default`, `class` and `return` are also refused. Choose a descriptive key such as `customerTier`. * Two rows cannot share a key: "A property with this key already exists." The pre-filled key comes from the schema and drops any dash, so a schema slugged `abandoned-cart` suggests `abandonedcart`. Rename it in the **Key** field before you press **Add** if you would rather have something else, because a key cannot be changed afterwards. ### Choosing between one and a list A list is right when the email loops over it, such as the lines on an order. The whole list cannot be the recipient: the final path must resolve to one contact or an email-format value. If it ends at the array itself, validation reports "Recipient path should not point to an array of records, it should point to either a single contact record or an email address value." ### Leaving a JSON shape open An open shape accepts any JSON, which is convenient and costs you the safety net. maxclicks can only warn you about a misspelled variable when it knows the shape, so with an open one `{{ payload.tpyo.name }}` passes silently and renders empty. Define the shape for anything you will reference by name. ## Remove a property Untick the row. That is the only way, and there is no delete button, no confirmation, and no undo. Removing a property also removes its expansion selection and clears a recipient pointing to it. Existing Liquid expressions remain in the content but lose their data source, so they can render empty. Review **Personalize** and **Checks** and repair those references after changing the model. There is no rename either. Renaming is removing and adding, with all of the above. On a standalone template, taking the last property off the list does not stick. Reopen the editor and a property for your space's default contact schema is back, with the recipient pointed at it. ## Choose who receives it **To** sits at the top of the panel. It is a chain: pick a property, and if it can be opened further, another dropdown appears after a chevron for the next step down. Only two things qualify as a recipient: * **A single contact record.** Not a list, and not an object or event record. * **A value declared as an email address**, including an email-format JSON value or a stored email field reached through a record. The Data path picker lets you navigate the declared data; recipient validation determines whether the final path is usable. Selecting a plain string, object or list does not make it a valid recipient. If there is no eligible row, the code editors say so directly: "No recipient is available yet. Add a contact record property or an email address property to the data representation to pick a recipient." Leaving it empty gives you "Recipient path is not provided." next to the **To** row. The blank-looking dash entry at the top of the dropdown is the clear option, and picking it does exactly that. ### Contact or bare email address The two are not interchangeable. | | A contact record | An email address value | | ----------- | -------------------------------------------------------------- | --------------------------------------------------------------------- | | Consent | Marketing checks the contact's subscription and selected topic | No contact subscription; applicable delivery suppressions still apply | | Unsubscribe | A personal preferences link, managed for you | No personal link | | Marketing | Allowed | Refused | | Reporting | Opens and clicks attach to that contact | Not attached to anyone | Marketing email can only go to a contact, because it must carry a working unsubscribe link. Point a marketing email at an address value and you get "This is a marketing email, so it can only be sent to a subscribed contact (marketing emails carry a required unsubscribe link). To send to a direct email address, use a transactional email template instead." Marketing or transactional is fixed when the email is created, so the fix is a new email, not a setting. A recipient path is separate from the expansion ticks used by the content. The picker initially exposes the available record and stored-field structure; an evaluated relationship is not automatically looked up because its name appears in your email. Review the configured path and its validation when using a related contact as the recipient. In the MJML and HTML code editors there is a second **To** control in the block above the code, next to **From**. It sets the same thing but only one level deep, so if your recipient reaches deeper than one step, that control shows nothing selected. Touching it replaces the deeper setting. ## Pull in computed values Open a record row and you see its fields. Stored writable fields are ticked and locked because they travel with the record. [Evaluated fields](/evaluated-attributes) must be requested through expansion. Some resolve an on-demand lookup; others read a published scheduled calculation. Ticking a field requests its configured behavior, not a fresh calculation. An evaluated relationship that returns records opens those records' fields underneath, where you can select further expansions. This is [attribute expansion](/attribute-expansion), and that page covers it properly. On-demand fields run their configured lookup when expanded. Scheduled Evaluated fields read the publication selected for that execution. An enabled calculation with no complete result or an expired result blocks expansion; it does not silently become an empty merge field. A valid publication with no result for this record can return null. Paused calculations can retain their last published result under that captured policy. Inspect **Published value** and **Data through** before relying on the field. Nested lookups add work, and a many-record result expands at most 1,000 records. Select only what the email uses. See [freshness and execution snapshots](/attribute-expansion). Ticks stay editable everywhere, including on a broadcast whose list is otherwise frozen, for as long as the email is still editable. ## What each place locks This is where most confusion starts. The same panel is locked differently depending on what is sending the email. | | Broadcast | Template | Workflow send-email step | | --------------------- | ------------------------------------------------------------------------------- | ------------ | ----------------------------------- | | The list | Fixed to the audience contact, reach the rest through selected Evaluated fields | Yours to set | Offered by the step, yours to adopt | | **Add property** | Not shown | Shown | Shown | | **To** | Locked to the audience contact | Yours to set | Yours to set | | Evaluated field ticks | Yours, while it is still a draft | Yours | Yours | ### A broadcast decides both for you A broadcast already knows who it is mailing, so the list is one row for the audience contact, named after that schema (a `Learner` audience gives you `learner`), and the recipient is that contact. The property checkboxes are disabled, there is no **Add property**, and **To** is read-only. The sentence above the list says the properties come from the sending context. The initial broadcast key is generated from the schema slug, replacing separators with underscores: `abandoned-cart` becomes `abandoned_cart`. Once saved, that email keeps its property key when the schema is renamed. Use the key displayed in **Data**, rather than reconstructing it from the current schema name. A broadcast supplies the contact for each selected recipient. Any additional customer context needs to be reachable from that contact rather than supplied as a second unrelated input. **To add a lookup or calculated value, expand the contact row and select the Evaluated field the email needs.** An evaluated relationship is how a broadcast reaches another record: give `Learner` an evaluated `cohort`, tick it, and write `{{ learner.cohort.name }}`. An on-demand relationship resolves for each recipient during expansion. See [computed values](#pull-in-computed-values) above and the [evaluated attributes](/evaluated-attributes) reference. If you need a genuinely free data model, with records that have nothing to do with the recipient, that is what a [template](/templates) is for: your own code passes the data in on each send. If either the list or the recipient drifts from what the broadcast wants, a repair appears above the list, which stays visible while you read it: * **Data out of sync**, reading "The data provided to this template has changed since it was set up. Sync the template to match it.", with a **Sync data properties** button. * "The recipient no longer matches the one enforced by the sending context.", with a **Sync recipient** button. Once a broadcast is scheduled or sending, the whole panel goes read-only: no ticks, no record pickers, no editing the sample. ### A template sets its own A standalone template owns both. Whatever you put on the list becomes what a send has to carry, so adding or removing a row here changes what your own code, or an integration, has to send. Every key on the list is required in the call, though each record only needs enough to identify it. A new template does not start empty: it seeds one property for your space's default contact schema and points the recipient at it. ### A workflow step offers, it does not impose A send-email step in a [workflow](/workflows) shows you what the step actually receives, and leaves the choice to you. Each thing the step carries is a row you can tick to take. Rows you have taken that no longer match what arrives are marked, which is where the drift shows up. **Add property** cannot create data for an earlier workflow step. Anything you add that the step does not carry turns red with "This property is expected but not provided.", and if you leave it there the step fails when the workflow runs. Only take rows the step gives you, and if you need something else, add a step before this one that fetches it. The step also has its own recipient control, **Sends to**, in the sidebar outside the editor. It sets the same thing as **To** in the panel, but only offers what the step carries, and only one level deep. Changing the workflow upstream changes what this step carries, quietly. Rows can go red on their own. Where two branches meet, only what both branches carry survives, so a property that exists on one path and not the other disappears from the step entirely. ## Preview it with sample data The **Sample** section below the data model holds [sample data](/editor/sample-data): a stand-in value for each property so the editor can render a preview. **Edit as JSON** opens the raw sample and generation controls; **Hide JSON** returns to the record pickers. The data model remains above it. Pick a real record with the picker on its row. The editor copies stored fields and requests the selected Evaluated expansions. The preview uses those sampled values; a real send resolves its execution data separately. Selecting another field requests a sample refresh, but it does not force a scheduled evaluation. Inspect the sample itself and the field’s publication status when a selected value cannot be loaded. A test send does not check any of this. It mails whatever the preview rendered, so it tells you nothing about whether the recipient is set correctly. ## Read the panel when something is wrong The **Checks** tab covers three separate data concerns: the model, the recipient and the sample. A correct-looking list does not establish that the recipient or sample is valid. | What you see | What it means | What to do | | ------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | --------------------------------------------------------------------- | | "Recipient path is not provided." | Nothing is in **To** | Pick a contact row or an email-address row | | "Unknown key." on a **To** step | That step points at something no longer there | Repick from the dropdown | | "Undefined path item." on a **To** step | The path carries on past a plain value | Shorten it back | | "Unknown schema" as a row's type | The schema behind that row was deleted | Remove the row and add a replacement | | "This property is expected but not provided." | The email wants something the sender does not carry | Untick it, or add it upstream | | "The expectation of this property is not compatible to its provided form. Update it accordingly." | Same key, different shape | Tick the second row for that key, the one the sender actually carries | | "The expectation of this property is compatible to its provided form, but not exactly the same. You may update it accordingly." | Close enough to work, worth aligning | Same, when convenient | | **Data out of sync** | A broadcast's list drifted | **Sync data properties** | | "Sample data does not satisfy the expected data representation with the specified attribute expansions:" | The sample no longer fits the list | Fix or regenerate the sample | When a shape mismatch appears, the same key shows up twice in a row, once ticked and once not. The lower one is what the sender actually carries. Tick it to take it. The **Checks** tab groups these under **Recipient**, **Data** and **Sample & preview**. Their actions open **Data**. For a sample problem, choose **Edit as JSON** in its **Sample** section. On narrow windows the side tabs collapse to icons. Choose **Data** to open the panel over the email, then close it to return to the full canvas. ## Why a later refresh may not change an active send A standalone sample resolves selected scheduled Evaluated fields against its current read context. A broadcast revision or root workflow run captures the calculation generation, freshness policy and observation time on its first read; child runs inherit that context. Publishing a newer calculation does not replace that captured selection. This fixes the scheduled calculation context, not every live customer field or on-demand lookup. An enabled calculation with no complete publication or an expired publication fails the expansion for that decision. Refreshing the calculation later does not repair an already captured unavailable selection. A paused definition can use its captured last-published policy; a valid publication with no subject result can return null. An erased pinned generation fails rather than switching to a newer one. Check the record’s **Published value**, **Data through** and status, then compare the sample with the execution context. See [attribute expansion](/attribute-expansion). ## Related * [Sample data](/editor/sample-data): the stand-in values the preview renders against. * [Attribute expansion](/attribute-expansion): selected fields, dependencies and execution freshness. * [Evaluated attributes](/evaluated-attributes): on-demand relationships and scheduled event-history measures. * [Liquid](/liquid) and [Adaptyle](/adaptyle): using what you declared here in the content. * [Schemas](/schemas): the records behind every record row. --- # Dynamic Composer Source: https://docs.maxclicks.ai/editor/dynamic # Dynamic Composer Use the Dynamic Composer for a designed email with responsive sections, columns and content blocks. The visual editor lets your team inspect the layout while maxclicks compiles its MJML into HTML for email clients. A new email is not blank. It starts with a brand logo block (only when your [brand kit](/brandkits) has a logo) and a `Hello!` text block. The editor saves as you type. ## Build a simple product announcement 1. Replace the starter greeting with a short announcement and set the subject. 2. Add an **Image** for the product, supply descriptive alt text, then add a **Button** with the product page’s full URL. 3. Use one section for the introduction and another for the image and button. Add columns only if the image and copy need to sit side by side. 4. Pick the sender and sample contact, then inspect the rendered preview at a narrow width. Check the button destination and image fallback in a real **Send test**. The result is one reusable layout whose customer details come from **Data**. Add a [Liquid fallback](/liquid#give-it-a-fallback) before using optional names, and mark an [Adaptyle](/adaptyle) section only when its wording should change per recipient. ## Structure Content nests in three levels. | Level | Role | | ------- | ----------------------------------------------------- | | Section | Full-width horizontal row. Holds columns and blocks. | | Column | Side-by-side region inside a section. | | Block | A single piece of content inside a section or column. | ## Blocks | Block | Content | | --------------- | --------------------------------------------------------------------------------- | | `text` | Paragraphs and headings (H1 to H6), bulleted and numbered lists. | | `button` | Call-to-action with text, link, colors, border, radius, padding, font, alignment. | | `image` | Uploaded or external URL, alt text, width, height, alignment, Fluid on mobile. | | `divider` | Line color, style (solid, dashed, dotted), thickness, surrounding spacing. | | `spacer` | Fixed vertical height in pixels. | | `youtube video` | A YouTube URL rendered as a static, clickable thumbnail card. | | `x post` | An X post rendered as a static card: author, handle, avatar, text. | | `link preview` | A link rendered as a preview card: image, title, description, site name. | | `liquid` | Raw [Liquid](/liquid) inline or as a Liquid Tag Section. | All blocks accept [Liquid](/liquid) and [Adaptyle](/adaptyle) syntax. ## Adding blocks Three ways to insert: * Hover an existing block and click the `+` icon. * Use the toolbar above the editor. * Type `/` to open the slash menu. The slash menu groups items: | Group | Items | | ------- | ------------------------------------------------------------------------------------ | | Text | Text, Heading 1 to 6, Bulleted list, Numbered list | | Media | Image, YouTube Video, X Post, Link Preview | | Layout | Section, Button, Divider, Spacer | | Utility | Variable, If Condition, For Loop, Liquid Section, Liquid Tag Inline, Assign Variable | Type to filter. Arrow keys move, `Enter` confirms. ## Sections Hover a section to reveal its `+` button. Click it to insert after, or Alt-click (Option-click on macOS) to insert before. Two types: * **Content Section**: standard container for blocks and columns. * **Liquid Tag Section**: a section driven by raw Liquid. Sections take a background color or image, padding, borders, and spacing. Select several sections and choose **Wrap Sections** to group them, so one background color or pattern runs across the whole group. **Unwrap Sections** undoes it. ## Columns Add a column inside a section from the `+` button on an existing column. It offers **Content Column** and **Liquid Tag Column**, inserting after the hovered column, or before it with `Alt` (`Option` on macOS) and click. Drag a column by its handle to move it beside another column. Drag the divider between columns to resize. Each column has one **Width** field that takes a pixel or percentage value (`200px` or `50%`). Percentage widths stay responsive as the email resizes. Select columns and choose **Wrap Columns** to keep them side by side as a group. **Unwrap Columns** undoes it. On mobile, columns stack vertically. Use **Wrap Columns** to keep a group side by side instead. Email-level **Design → Document** includes **Stacks below**, the width at which columns stack, alongside the email width and canvas, background and text colors. ## Selection menu Select text to open the selection menu. From it you: * Add or edit a **Link**. * Toggle **Bold**, **Italic**, underline, and strikethrough. * Wrap the selected sections in an **Adaptyle Section** with the **Adaptyle** button (it reads **Remove Adaptyle** once one is applied). * **Wrap** or **Unwrap Sections**, **Wrap** or **Unwrap Columns**. Text size and color, font family, weight, line height, letter spacing, and alignment are in the right-hand panel. Text converts to a heading or list from the same panel or the slash menu. ## Adaptyle sections An **Adaptyle section** is an [Adaptyle](/adaptyle) prompt attached to the sections it wraps. It defines which sections may be rewritten using the recipient’s data. Rendering checks protect the surrounding content and layout; inspect the generated preview before sending. To drop the prompt and keep the content as written, select text inside it and choose **Remove Adaptyle**. Write the prompt in plain language, and attach reference text when the AI needs more to go on. The **Subject** and **Preview** fields take Adaptyle prompts directly. Nothing is personalized as you type. It happens when you open the preview panel and click **Generate preview**, which spends credits and runs against your [sample data](/editor/sample-data). See [previewing before you send](/editor#preview-it-before-you-send). Use the maxinja input below the editor tabs to refine the selected subject, section or block. The scope label and selection trail identify the target. “Shorten this section, keeping the offer and button unchanged” edits the saved draft. Add Adaptyle only when that passage should be generated again for each recipient. ## Keyboard shortcuts | Mac | Windows | Action | | ----------- | -------------- | --------------------------- | | `/` | `/` | Open the slash menu | | `⌘ B` | `Ctrl B` | Bold | | `⌘ I` | `Ctrl I` | Italic | | `⌘ U` | `Ctrl U` | Underline | | `⌘ L` | `Ctrl L` | Insert Liquid variable | | `⌘ Shift L` | `Ctrl Shift L` | Insert Liquid tag | | `⌘ Alt 0` | `Ctrl Alt 0` | Convert to paragraph | | `⌘ Alt 1-6` | `Ctrl Alt 1-6` | Convert to heading (1 to 6) | ## Reordering Hover a block, section, or column to reveal a drag handle on the left. Drag it to reorder. Switching out of the Dynamic Composer is one-way. **Switch editor** offers **Code Editor**, which writes your layout out as MJML source, and **Simple HTML**, which flattens it into the [Plain composer](/editor/plain) block list. Neither can be undone. **Simple HTML** also drops content: the Plain Composer has no preview text and no links on images, so your preview text and every link you set on an image are discarded in the move. The confirmation dialog says so before you commit. ## Related * [Email editor](/editor): modes, branding, recipient, preview, and API. * [Adaptyle](/adaptyle): how AI personalization works. * [Liquid](/liquid): variables and tags. * [Attributes](/attributes): fields you can reference. --- # Fonts Source: https://docs.maxclicks.ai/editor/font # Fonts Typography is part of the brand your customers recognize. Set shared font choices in a [brand kit](/brandkits) style pack, or use a visual block's **Typography** settings for a specific treatment. The receiving email client determines which available font is displayed. Email is not the web. An inbox renders the fonts installed on that device, so no font choice is a guarantee. Every pick you make therefore saves a whole chain of fallbacks, not one name, and your reader lands in a related family wherever the first choice is missing. ## Widely available fonts The first group in the picker has no warning icon. These common font families are a useful starting point; the saved fallback chain still determines what appears when the first family is unavailable. Arial, Courier New, Georgia, Lucida Sans Unicode, Tahoma, Times New Roman, Trebuchet MS, Verdana. ## Fonts that usually render The **Web** group adds 26 families that sit on most desktops but are not guaranteed in every email client: Calibri, Cambria, Candara, Century Gothic, Consolas, Constantia, Corbel, Segoe UI, Palatino Linotype, Book Antiqua, Garamond, Franklin Gothic Medium, Arial Black, Arial Narrow, Impact, Lucida Console, Lucida Grande, Helvetica, and others. Each carries a safe fallback of its own, so Calibri drops to Arial, Consolas drops to Courier New, and Cambria drops to Georgia. Pick one of these when the fallback would still be acceptable to you. ## Google fonts Three web fonts sit in their own group: **Lato**, **Merriweather**, and **Roboto**. They are loaded as a stylesheet rather than read off the device, which some email clients allow and others quietly ignore. If you type a weight by hand under **Typography**, these are the weights each one actually ships, in normal and italic: | Font | Weights | Falls back to | | ------------ | ---------------------------- | ------------- | | Lato | 100, 300, 400, 700, 900 | Arial | | Merriweather | 300, 400, 700, 900 | Georgia | | Roboto | 100, 300, 400, 500, 700, 900 | Arial | The picker marks the Web and Google groups with a warning icon for a reason: some email clients do not support these fonts and will fall back to a similar one they do support. maxclicks keeps no list of which client does what. Send yourself a test and look at it in the clients your audience actually uses. ## Setting the font for a whole email A brand kit style pack has a **Heading** and a **Text** section, each with its own **Font Family** and **Font Size**. Content that references the style pack uses its typography at render time. Hard-coded block styles remain explicit overrides; already sent emails and frozen broadcast content do not change when you edit the kit. [Forms](/forms) follow the same style pack. A form with no style pack behind it falls back to the reader's own system font. ## The labels beside each font Each entry shows a category: Sans-Serif, Serif, Monospace, Casual Script, Script, or Fantasy. It is there to help you pick a font of the right character and changes nothing about what is saved. The greyed-out **Custom font** entry at the bottom of the list is not available: there is no font upload today. Choose from the three groups above. --- # HTML code editor Source: https://docs.maxclicks.ai/editor/html-code # HTML code editor Use HTML Code when your team already has email markup or needs direct control over its layout and inline styles. Bring the HTML into the shared composer, connect the customer data and add personalization where it serves the message. Your HTML defines the layout, while Liquid, any Adaptyle instructions and the sending footer are resolved during rendering. Test the result in representative email clients; this editor does not create responsive layouts for you. If you would rather have that handled for you, use the [MJML code editor](/editor/mjml-code) instead. ## Write the subject and the body The editor has two fields. **Subject** is a single line. **Body HTML** is the full markup, in a code area with line numbers and a minimap. Both fields accept [Liquid](/liquid) and [Adaptyle](/adaptyle), so the subject is personalized the same way the body is. The editor colors all three languages together as you type, and underlines Liquid that will not resolve against your [sample data](/editor/sample-data) before you ever render a preview. There is no auto-format here. Lay out your markup the way you want to read it, because maxclicks will not reindent it for you. The [MJML code editor](/editor/mjml-code) behaves the same way. ## Set the sender and the recipient **From** and **To** sit above the subject, the same as in every other editor. See the [editor overview](/editor) for what each one accepts. ## Use your brand kit instead of hard-coded values The brand kit you select in email setup arrives as `branding`, so write `{{ branding.logoUrl }}` rather than pasting the link. Colors and fonts come through the same way. References resolve when the email is rendered; sent emails and frozen broadcast content keep their existing values. See [brand-kit changes](/brandkits#know-what-changes-when-you-edit-or-delete-a-kit). Give every image a descriptive `alt` attribute. It is what screen readers announce, and it is all your reader sees in the many clients that block images by default. ## Coming from the Plain Composer The [Plain Composer](/editor/plain) can hand its blocks over to this editor, written out as HTML. That switch is one-way. Once the blocks are markup there is no way back to them, and maxclicks asks you to confirm before doing it. ## Work within the email renderer’s limits Use email-safe HTML with absolute resource URLs. Final rendering rejects active content such as scripts, iframes, forms, event-handler attributes and unsupported URL schemes. It also enforces size and structure limits: a nonempty single-line subject up to 4,096 bytes, a final HTML artifact up to 1 MiB, at most 6,000 elements and nesting no deeper than 80 levels. During AI personalization, existing links, attributes, stylesheets and protected layout context must remain intact. Put prompts inside the text you want rewritten; change markup and destinations while authoring the draft. An “invalid final artifact” error requires fixing the content, even if a browser could display it. The **Outline**, **Personalize**, **Data** and **Checks** tabs share the same behavior as the [email editor](/editor). Ask maxinja to make a specific source edit, then render and send a test to review the compiled result. ## Related * [Plain Composer](/editor/plain): the block-based way to build the same HTML. * [MJML code editor](/editor/mjml-code): responsive layout without hand-writing tables. * [Sample data](/editor/sample-data): what the preview renders against. --- # MJML Code Editor Source: https://docs.maxclicks.ai/editor/mjml-code # MJML Code Editor Use MJML Code when your team wants to author a responsive email in markup. MJML describes the layout in email-specific components, and maxclicks compiles them into the HTML email clients receive. Customer data, brand controls and personalization use the shared composer. Reach for it when you already have MJML to paste in, when you need an attribute the visual builder does not expose, or when you prefer working in code. For the same layout built visually, use the [Dynamic Composer](/editor/dynamic). ## The tags you will use most | Tag | What it gives you | | -------------- | ---------------------------- | | `` | A full-width horizontal row. | | `` | A column inside a section. | | `` | A block of text. | | `` | A call-to-action button. | | `` | An image. | | `` | A horizontal rule. | | `` | Vertical space. | ## Write the subject and the body **Subject** is a single line. **Body MJML** is the markup, in a code area with line numbers and a minimap. Both fields accept [Liquid](/liquid) and [Adaptyle](/adaptyle), so the subject can be personalized as well as the body. The editor colors MJML, Liquid and Adaptyle together, and underlines Liquid that will not resolve against your [sample data](/editor/sample-data) before you render anything. There is no auto-format here. Lay out your markup the way you want to read it, because maxclicks will not reindent it for you. ## Set the sender and the recipient **From** and **To** sit above the subject, the same as in every other editor. See the [editor overview](/editor) for what each one accepts. ## Use your brand kit instead of hard-coded values The brand kit you select in email setup arrives as `branding`, so write `{{ branding.logoUrl }}` rather than pasting the link. Colors and fonts come through the same way. References resolve when the email is rendered; sent emails and frozen broadcasts keep their existing values. See [brand-kit changes](/brandkits#know-what-changes-when-you-edit-or-delete-a-kit). ## Coming from the Dynamic Composer The [Dynamic Composer](/editor/dynamic) can hand its layout over to this editor, written out as MJML source with your Liquid and Adaptyle intact. That switch is one-way. Once the email is MJML source there is no way back to the visual blocks, and maxclicks asks you to confirm before doing it. ## Work within the email renderer’s limits Use email-safe HTML with absolute resource URLs. Final rendering rejects active content such as scripts, iframes, forms, event-handler attributes and unsupported URL schemes. It also enforces size and structure limits: a nonempty single-line subject up to 4,096 bytes, a final HTML artifact up to 1 MiB, at most 6,000 elements and nesting no deeper than 80 levels. During AI personalization, existing links, attributes, stylesheets and protected layout context must remain intact. Put prompts inside the text you want rewritten; change markup and destinations while authoring the draft. An “invalid final artifact” error requires fixing the content, even if a browser could display it. The **Outline**, **Personalize**, **Data** and **Checks** tabs share the same behavior as the [email editor](/editor). Ask maxinja to make a specific source edit, then render and send a test to review the compiled result. ## Related * [Dynamic Composer](/editor/dynamic): build the same MJML visually. * [HTML code editor](/editor/html-code): hand-written HTML, where the responsiveness is yours. * [Fonts](/editor/font): which fonts survive the trip to an inbox. --- # Plain Composer Source: https://docs.maxclicks.ai/editor/plain # Plain Composer Use Plain Composer for a direct, written message: a product update, a founder note or a focused follow-up. Its visual HTML blocks keep the layout simple while retaining the shared composer's customer data, brand and personalization controls. Content runs top to bottom in one straight list. There are no sections or columns, and no layout to fuss over. ## Write a first customer note Start with a short subject, a greeting, one paragraph explaining the update and a button to the next action. Select the sender and a sample customer in **Data**, then preview the message. For an optional first name, use a Liquid fallback so the greeting remains complete when the value is missing. Use **Send test** to check line lengths, links and spacing in an inbox. “Plain” describes the simple layout: this composer still creates HTML email and supports images, buttons and personalization. ## Add a block Two ways to insert one. Type `/` where you are writing and pick from the menu, filtering as you type and confirming with `Enter`. Or hover an existing block and click the `+` beside it, which inserts after that block, or Alt-click (Option-click on a Mac) to insert before it. The same hover reveals a drag handle. Pick it up to move the block somewhere else in the list. The menu is grouped: | Group | What you get | | ------- | -------------------------------------------------- | | Text | Text, Heading 1 to 6, Bulleted list, Numbered list | | Media | Image, YouTube Video, X Post, Link Preview | | Layout | Button, Divider, Spacer | | Utility | Variable, Liquid Tag | Images take a source, alt text and dimensions. YouTube videos, X posts and links render as static preview cards with a link to the source. Columns and side-by-side layout do not exist in this composer, by design. If you need them, start the email in the [Dynamic Composer](/editor/dynamic) instead. You cannot move an email from here to there later. ## Format as you write Select text and a small menu appears with a link, bold, italic, underline and strikethrough. Everything else lives on the block itself. Click the handle beside it and you get **Typography** for the font, size, weight, line height and letter spacing, and **Turn into** for changing a paragraph into a heading or a list. ## Personalize a passage Select the text you want written per recipient and press **Adaptyle** in that same menu. That marks the passage for rewriting per recipient using the selected data. Rendering checks protect the surrounding fixed content; inspect the resulting preview. Select it again and the button reads **Remove Adaptyle**, which drops the instruction and keeps the words as written. The **Subject** field takes an Adaptyle instruction directly. See [Adaptyle](/adaptyle) for how to write one. ## Drop in a value you already have Insert **Variable** for a single value, like a first name, and **Liquid Tag** for logic such as an `if` or a `for`. Both come from [Liquid](/liquid), and both can also be typed inline while you write. The fields available to you are whatever the email's data list says it needs, plus your brand kit under `branding`. Open **Preview** to resolve Liquid against your [sample data](/editor/sample-data). **Base** needs no writing-model call. When the email has Adaptyle instructions, **Generate preview** requests the generated wording and spends credits; it is a separate action. ## Refine a block with maxinja Select a block and use the maxinja input at the bottom of the right panel. Its label follows your selection; a request there carries the current email and, when available, the selected block. State what should change and what should stay fixed. Use **Checks** to review readiness after the edit and **Preview → Personalized → Generate preview** to generate a recipient sample. The static media cards link to the original content. They do not put a working video player, live X feed or interactive web page into the delivered email. ## Moving to code later **Switch editor** offers **Code Editor**, which writes your blocks out as HTML into the [HTML code editor](/editor/html-code). That switch is one-way. Once the blocks are markup there is no way back to them, and maxclicks asks you to confirm before doing it. ## Related * [Dynamic Composer](/editor/dynamic): sections, columns, and full layout control. * [HTML code editor](/editor/html-code): hand-write the raw HTML body. * [Adaptyle](/adaptyle) and [Liquid](/liquid): personalization. --- # Sample data Source: https://docs.maxclicks.ai/editor/sample-data # Sample data Sample data lets your team inspect an email before choosing to send it. Use a representative customer and the fields the template expects, then check the rendered subject, content and links. A sample is review material; real recipient data is resolved for the actual send. ## Preview two customer situations 1. Open **Data**, then choose a customer under **Sample**. 2. Open **Preview**. Inspect **Base** for fixed content and Liquid; use **Generate preview** when Adaptyle should write the personalized version. 3. Switch to a customer with less data, or use **Edit as JSON** to remove an optional value from the sample. Regenerate personalized content and check the fallback. 4. Use **Send test** after the rendered result is ready to inspect it in an inbox. For an announcement, the two useful outcomes might be a greeting with the customer’s name and a general greeting when the name is missing. Editing this sample does not edit the customer record. ## What an email needs Every email template lists the information it needs, and it can only reference what is on that list. A welcome email needs a `contact`. A receipt might need a `contact`, an `order`, and a list of `items`. The template reference calls this list `expectedDataRepresentation`; a send supplies matching keys under `data`. Each entry is either a record or a plain JSON value. | Type | Fields | Sample value | | -------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------- | | `record` | `key`, `schema` (the schema's slug), `isArray`, `description` | A [record](/schemas) from that schema (contact, object, or event), or a list of them. | | `json` | `key`, `jsonSchema`, `description` | A plain value matching the JSON schema. | Your sample is a JSON object with one key per entry. Each value has to fit: required fields present, the right types, and any constraint honored (a set list of allowed values, an email format). Where a value does not fit, that part of the preview cannot render. maxclicks adds a `branding` entry (your brand, brand kit, colors, and fonts) when it renders. It is reserved and appears in the editor as a note you cannot edit. Do not set it in your sample. ## Where you edit it Open the editor’s **Data** tab. The upper section lists the email’s expected properties and **To** recipient (called `recipientDataPath` in the reference). The **Sample** section below it holds the record pickers. Choose **Edit as JSON** to open the raw `sampleDataStringified` value in a JSON5 editor, with record pickers and AI generation controls. Choose **Hide JSON** to return to the compact sample view. Editing the JSON changes the preview, not the selected customer record. ## Using a real record In both views, each record entry has a picker listing records from its schema. Pick one and its fields are written into the sample under that entry's key. Contact and object entries you have not chosen yet fill themselves with the first available record, so the preview starts populated. Event entries are left for you to choose. Opening **Edit as JSON** fills anything still empty, events included, with a record picked at random from the first ten. **Edit as JSON** also has a shuffle control that refills every record entry at once. In the sample pickers, an entry whose schema holds no records shows a **Generate** action instead of a picker. The record's stored writable fields are copied across as they are. [Evaluated fields](/evaluated-attributes) follow the expansion ticks. On-demand fields run their lookup; scheduled fields read a complete publication. Selecting a field does not recalculate it. An unavailable or expired enabled calculation cannot supply a valid real-record expansion; inspect **Published value**, **Data through** and the evaluation status. Unselected fields are not available for personalization. Ticking a field after choosing a record requests a refresh of that sample’s expansion. ## Generating with AI The editor can write the sample for you. In **Edit as JSON**, open **Generate sample data by AI**. It reads the list of what the email needs and your [schemas](/schemas), then invents believable values: names, dates, emails, all within the limits each field allows. maxclicks checks the result against that list before accepting it. The dialog has an optional prompt field. Leave it blank for ordinary data, or describe the scenario you want: ```text Generate data for a user who just signed up, include a welcome offer code. ``` The **Generate** action beside a single entry (shown when its schema holds no records) fills that one entry, with no prompt. Generating a sample is an AI action and spends credits from your space. Writing or editing the JSON by hand costs nothing. ## Build a useful review set Use real records to test your data mapping and invented samples to test edge cases. An AI-generated record is not evidence that a matching contact or order exists, and editing the sample does not update your customer records. For a win-back email, review a frequent buyer, an occasional buyer and a contact with no usable purchase history. Include missing first names, empty related-record lists and null subject results from a valid calculation. Give Liquid a fallback and give Adaptyle instructions for that missing context. Test unavailable or expired scheduled evaluations separately: those are execution-readiness failures, not ordinary empty customer fields that a greeting fallback can repair. Generate the personalized preview again after changing the sample. **Send test** sends the rendered sample to your chosen test address; it does not run the production recipient lookup or prove that the recipient has marketing consent. See the [editor’s test-send behavior](/editor#preview-it-before-you-send). ## Keeping the sample in sync What the email needs, and who it is addressed to, can drift away from where it is being sent. A [broadcast](/broadcast) fixes both to the contact in its audience, so the editor shows: * **Data out of sync**: a **Sync data properties** action that resets the list to match the audience. * **Sync recipient**: appears when the email is addressed to something other than the audience contact. A standalone [template](/templates) sets its own list and its own recipient, so neither is forced. ## Why a later refresh may not change an active send A standalone sample resolves selected scheduled Evaluated fields against its current read context. A broadcast revision or root workflow run captures the calculation generation, freshness policy and observation time on its first read; child runs inherit that context. Publishing a newer calculation does not replace that captured selection. This fixes the scheduled calculation context, not every live customer field or on-demand lookup. An enabled calculation with no complete publication or an expired publication fails the expansion for that decision. Refreshing the calculation later does not repair an already captured unavailable selection. A paused definition can use its captured last-published policy; a valid publication with no subject result can return null. An erased pinned generation fails rather than switching to a newer one. Check the record’s **Published value**, **Data through** and status, then compare the sample with the execution context. See [attribute expansion](/attribute-expansion). ## Related * [Liquid](/liquid): reference record fields in content. * [Adaptyle](/adaptyle): AI personalization on top of the template. * [Schemas](/schemas) and [attributes](/attributes): where the records in a sample come from. --- # Evaluated Attributes Source: https://docs.maxclicks.ai/evaluated-attributes # Evaluated attributes Define a customer fact once and use it in an audience, email or journey. An evaluated attribute can find related records or calculate an answer when you need it. You describe the question; maxinja generates the logic; you check the result. An evaluated attribute has generated logic and a timing rule. This page explains **on-demand** relationships and calculations. A **scheduled** event-history definition publishes results in the background and has separate [publication and freshness rules](/attributes#computed-attributes). Both appear as evaluated attributes in the app; review their timing before relying on a value. ## Example: has this person created a project? Loopwell wants to contact new users who have not created their first project. Counting projects across the whole account would miss the difference between someone’s own progress and a teammate’s work. Define **Own projects** as “Count projects created by this person.” The Loopwell demo records return: | Customer | Shared account | Own projects | First-project audience | | ----------- | -------------- | ------------ | ---------------------- | | Maya Nolan | Studio North | 0 | Included | | Daniel Chen | Studio North | 1 | Excluded | The account is the same. The answer changes because the definition follows the person who created each project. The demo calls this field **Project count**. These are verified values from fictional demo records, not customer results. ## Before you begin You need customer and project records with an identifier that links a project to its creator. An account relationship alone cannot answer who created it. Check the source values before generating the definition; maxclicks cannot infer an absent relationship. Give the field a clear name, such as **Own projects**, and decide what it should return: a number, one related record or a list. For this count, a customer with no matching projects should return `0`; an unavailable or failed calculation is not the same as zero. ## Create one Open **Settings → Schemas**, choose a schema, then **Attributes → Create attribute**. Event payload attributes are supplied by the producer; create reusable evaluated context on a contact or object schema. Choose **Evaluated attribute** under **How is the value set?**, give it a name and stable key, and describe the answer. For the Loopwell example, describe the count: > Count Project records created by this User, using the project’s creator relationship. Do not include projects created by other users in the same account. Return a number, or zero if no projects match. Use your actual schema names and creator identifier. Reference this record’s fields with `@` where available. Explain which source keys identify the same person; similar field names do not create a join automatically. The generated definition contains a readable description and a badge for the return type. maxinja chooses either a schema target with **one** or **many** cardinality, or a JSON result with a defined shape. Review that choice as carefully as the business rule. An email expecting one subscription needs different content from one receiving a list. Refine the definition if the match key, date window, status filter, order or limit is wrong. Generate the refinement, then save the field. A new prompt alone is not a new saved calculation. ### Check the definition against real records The generated expression stays on maxclicks servers. The app exposes the definition for your review. Validate it with real records: one expected match, one exclusion, and one missing-data case. For a latest or top-N relationship, include a record with several possible matches so you can verify the ordering. For **Own projects**, compare a customer with zero projects and one with a known project. Include two people from the same account, as in Maya and Daniel’s case, to catch an account-wide count. Check an absent creator identifier separately. The readable definition explains intent; the returned values establish whether that intent matches the data you actually hold. Use both before sending. ## What you can build on it Reuse **Own projects** wherever the next decision needs this person’s progress: | Where | How the same field is used | | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | | [Segments](/segments) | Include new users whose own-project count is zero. Exclude Daniel even though he shares Maya’s account. | | [Templates](/templates) and [broadcasts](/broadcast) | Select the field in **Data** so [Adaptyle](/adaptyle) can use the customer’s progress when writing the message. | | [Workflows](/workflows) | After a wait, request the field again and stop the first-project reminder if the count is now above zero. | This reuses the business definition. It does not freeze one answer everywhere: a later request can see newer source records. For a customer whose count has changed, re-check the next decision before sending. ## Return a related record or list A relationship is a question with a record as its answer: “Which account does Maya belong to?” or “Which orders belong to Sarah?” The contact remains the recipient. The returned object supplies facts such as the account plan or order contents. You store the match keys, then describe how to compare them. The rule can link a contact to objects or one object to other records; matching field names alone do not establish a connection. For example: * **One subscription:** “Return the Subscription whose externalId equals this Customer’s subscriptionId. Return no record if there is no match.” * **Recent orders:** “Return this customer’s three most recent completed Orders, matching Order.customerId to Customer.userId, newest first by purchasedAt.” Those examples require the named schemas and identifiers to exist. Review the target record type, **one** or **many** cardinality, sorting and limit before using the result. A record relationship resolves the current record when requested. Changing a company's name changes the value seen on a later lookup; the relationship is not a copied company profile. Use [attribute expansion](/attribute-expansion) to select the relationship and any nested fields an email or workflow needs. A one-record relationship with no match yields no record. A many-record relationship yields a list; write an explicit fallback for an empty list. ## One place it will not appear The public Records API does not include evaluated attributes in ordinary record reads or lists. Selecting a field in an email's **Data** panel configures that email's rendering context; it does not change the public record response. ## What it costs and how it behaves at read time Generating and refining a definition uses AI credits. Reading the saved definition executes data logic rather than asking a model to infer a new answer. That can still involve several lookups, particularly for nested relationships. Each requested evaluation uses the source records available at that time. Two steps can see different results if records change between them. A fresh calculation cannot compensate for a source that has not synced its latest records. A many-record relationship returns at most **1,000 records**; excess results are truncated with a warning. Ask for a business-sized result such as the latest three orders instead of relying on that cap. Invalid result shapes or missing generated logic can fail the read rather than quietly produce an empty value. ## Update definitions and dependencies maxclicks records the schemas, attributes and segments a generated definition references. Those dependencies can block deletion or incompatible changes. Read the reported dependency and update the consuming definition before removing its source. Refine a definition when its business meaning changes; existing logic is not automatically regenerated from a revised description. Avoid circular relationships such as A requiring B while B requires A. Expansion detects a circular dependency and fails. ## Stored or evaluated | | Stored writable field | Evaluated attribute | Scheduled evaluated attribute | | ------------------------------ | -------------------------------------------------------- | -------------------------------------------- | ------------------------------------------- | | Source | Your supplied value, or a creation-time default/autofill | Saved logic over available records | A typed calculation over one event source | | Refresh | Record write | When requested | Background cadence, then publication | | Result | Typed field value | One record, many records or JSON | Count, sum, latest value, or grouped values | | Empty case | Determined by missing-value policy and supplied data | No matching record or a defined empty result | No matching value is empty, not zero | | Record API writable | Yes | No | No | | Needs expansion for email data | No | Yes | Yes | ## Related * [Attribute expansion](/attribute-expansion): select fields and verify real values. * [Attributes](/attributes): stored types, creation defaults and scheduled-attribute publication. * [Schemas](/schemas): define the records and identifiers a relationship needs. * [Segments](/segments): apply customer context to an audience decision. --- # Events Source: https://docs.maxclicks.ai/events # Events Events bring customer behavior into your decisions: an order completed, a trial started, or a visitor viewed a product. The useful contract includes what happened, when it happened, which record it describes, and which facts the campaign needs. **Settings → Events** defines the events your backend, workflows, integrations and collection sources report. Each event has one schema and key. A browser or server collection binding uses that schema's attributes, rather than maintaining a second event definition. The producer path changes authorization and identity; the shared definition gives campaigns one business meaning. ## A thing and something that happened to it An Order object holds the current order: its status can change from placed to refunded. A PurchaseCompleted event records the earlier purchase, and a later RefundIssued event records the refund. Updating the object does not rewrite either occurrence. The same distinction works for software: an Account object holds today's plan; a PlanUpgraded event records when it changed. Use the object for current context and the event for the moment a journey should react to. ## Define the event Choose **Create event schema** for a business event such as `PurchaseCompleted`. Give it a name, plural name, slug and description. The slug is its stable API reference. Define one schema for one business meaning, and use properties for variations such as plan, currency or purchase channel. For website or server collection, create the same event schema, then configure its [Collection binding](#collected-events). The binding adds a source, associated record scope and optional context field. Do not create a duplicate PurchaseCompleted schema merely to accept it from another producer path. ## Add the fields it carries Open a business event schema's **Attributes** tab and add the [attributes](/attributes) the downstream decision needs. For an order, that may be order number, amount, currency and status. Declare each custom field before sending it; unknown custom fields are omitted with a warning. The event also has platform fields: | Field | Meaning | | ------------------------- | -------------------------------------------------------------------- | | `id` | maxclicks' identifier for the accepted event | | `eventId` | Your stable identifier for this occurrence, used for retry detection | | `occurredAt` | When the business action happened | | `receivedAt` | When maxclicks accepted it | | `readyAt` | When preparation completed; empty while it is not ready | | `contactId` or `objectId` | An explicit subject in the same workspace; supply at most one | | `readinessState` | Whether the event is ready for downstream use | | `definitionRevision` | The definition accepted for this occurrence | A subject ID is a maxclicks record ID, not an email address, your application's user ID, or an object's external ID. Resolve or create that record first. You can still store external identifiers as custom properties, but that alone does not establish the subject link needed by a profile calculation. Use `occurredAt` for behavioral windows. Omitting it uses the acceptance time. The business Events API accepts occurrence times up to 366 days in the past or five minutes in the future, and limits a single input to 256 KiB. See the [Events API](/api/events) for the full wire format and batch behavior. ## Fire it Business events can arrive through the Events API, an integration, or a workflow's **Fire Event** step. There is no CSV event import. A new occurrence is immutable; an update to your source record does not rewrite the old event. Acceptance and readiness are separate. An event whose missing fields require AI autofill can be accepted durably while preparation continues in the background. Only ready events are exposed to normal business-event reads and generated queries, and only readiness starts their downstream event delivery. | Readiness | What it means for your campaign | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `pending` | Accepted, still preparing; it is not yet a trigger-ready event | | `ready` | Prepared and available for queries and downstream event delivery | | `failed` | Preparation ended with an error; inspect the failure before relying on the occurrence | | `outcome_unknown` | A preparation outcome could not be established safely; reconcile the accepted occurrence rather than submitting a new occurrence ID | Erasure can remove the event record and retain an erased acceptance receipt. A later status read may therefore report no event, while a replay of its occurrence identity is rejected. A pending event can wait for available credits or processing capacity. An acceptance response does not prove that a workflow has run or that an email was sent. ## Make retries safe Set `eventId` once per occurrence and reuse it for retries of that same input. A network retry of `order-184-paid` should still mean the same purchase, with the same subject, properties and explicitly supplied occurrence time. maxclicks returns the original acceptance receipt for a duplicate and marks it as a duplicate. It rejects reuse of that ID with different input. A retry does not revise the accepted definition or replace its data. The receipt's readiness describes the moment of acceptance. To learn what happened afterward, use the event status operation in the [Events API](/api/events). A newly generated event ID means a new occurrence and can cause new automation. ## Watch them arrive Open a business event's **Logs** tab. Choose a date range and search by ID or payload; the histogram and paginated rows help you locate actual incoming activity. The library shows receipt volume, last receipt and declared properties. The library's health badges describe source activity and configuration: * **No events** means no recorded receipt for the schema. * **Archiving only** means activity is arriving without a published workflow or live webhook using the event as a trigger. * **Live** means activity is arriving and a trigger is configured. * **Silent** means the source has received events before but has been quiet long enough to flag it. These badges are not a per-occurrence delivery receipt. For a missing journey, trace the event ID through acceptance/readiness, then inspect the [workflow run](/workflows/run). Check the selected schema, subject, trigger condition and published workflow version. ## Collected events Use collected events for a typed activity stream from a site or server, such as product views or feature usage. In **Settings → Events → Sources**, choose **Connect source** and set: | Control | Decision | | ---------------------- | ------------------------------------------------------------------------------------------------------------ | | **Name** | A source your team can recognize, such as Storefront | | **Connection** | **Browser SDK** for anonymous browser observations, or **Server collection** for backend-authorized activity | | **Environment** | **Test** or **Production**; the creation default is Test | | **Collection enabled** | Whether the source accepts incoming observations | Connection type and environment are fixed after creation. Pausing a source rejects incoming requests; it does not collect them into a queue for later. Open the event's **Collection** tab and choose **Bind source**. Select the **Source** and **Associated records**. Browser sources associate with a contact schema; server sources can associate with contacts or objects. Each event has one collection binding. The event key is its existing schema slug, and its payload contract comes from **Attributes**. **Context attribute** optionally selects a JSON object attribute on the event. Producer context fills that attribute, which is excluded from producer properties. With **No context**, the producer must send empty context. For a product view, keep `productId` in event properties and route page/campaign information into a declared `context` JSON attribute. Required event fields use **Required in the event payload**. New event attributes are producer-supplied; their editor does not offer profile autofill or evaluated relationships. Collection validates the declared property shape and rejects undeclared properties. Native record input can report omitted unknown fields as warnings, so test the actual producer path rather than assuming both tolerate an extra property. Recorded occurrences cannot be backfilled. Adding a required field after this event type has recorded history is blocked, even if a new producer sample includes it. Keep the field optional or create a new event type for the new required contract. Saving a definition or binding change runs setup checks. Changes to fields used by workflows or scheduled evaluations need their dependents validated. A producer-facing contract change can require a passing source sample before application. Inspect **Changes**, coordinate the new producer payload, and review any collection pause or retry requirement before switching. **SDK collection enabled** controls the binding. Pausing it, or pausing its source, stops SDK collection; native server events, workflow events and calculations continue. Paused intake does not queue rejected requests for later replay. Existing accepted history is separate from the choice to accept new observations. ### Connect the source and establish identity On the source, use **Create write key**, set its lifetime, and copy the new value before leaving: it is shown only once. A source key can send every enabled event defined for that source. Keys show **Active**, **Expired** or **Revoked**. Install a replacement before revoking a working key when collection should continue. Keep server keys on your backend. Browser credentials authorize anonymous observations; they do not authorize a visitor to choose a customer identity. Your authenticated server establishes the connection between an anonymous identity and an existing contact. See the [identity operation in the Events API](/api/events) for the server-side contract. A collected observation does not itself subscribe a contact to email. Keep identity resolution, collection permission and communication [consent](/contacts#consent-who-you-are-allowed-to-email) as explicit decisions in your implementation. ## Start a workflow from it Add an **Event Fired** trigger and choose the event schema it should use. For a business event, a generated condition can narrow the trigger, for example “Only completed purchases over 100 EUR.” Review the actual currency and status fields so the condition means what your team intends. For collection input, verify the binding's associated schema and resolved subject as well as the trigger condition. Review and publish the workflow. Receiving observations before publication does not request a backfill of runs from earlier history. Later steps use the trigger's declared output. Inspect **Reads** and the email's **Data** tab before referring to the event's values in [Liquid](/liquid) or [Adaptyle](/adaptyle). Business events can also drive [webhooks](/webhooks); inspect the webhook's condition and delivery results separately. ## Turn event history into customer context Create an **Evaluated attribute** on the destination contact or object schema. Describe a count, sum or latest value from this event schema, plus its filter, rolling window and cadence. Review the generated schedule and destination before saving. The profile owns the result; the event supplies the history. For business event sources, only ready events within the occurrence-time window and explicitly linked to that profile qualify. A valid publication with no qualifying history gives an empty result rather than zero. No complete publication, or an expired enabled publication, blocks a new decision that requests it. Check **Data through** and the published status before using the value in an audience. The complete setup and freshness rules are in [Scheduled evaluated attributes](/attributes#computed-attributes). ## What you cannot undo You cannot edit a business event occurrence into a different fact. Send a new event for a subsequent business action, such as a refund, with its own occurrence ID. Deleting an event schema or erasing a linked record uses a deletion process that can also remove associated event history and derived data. Deletion is permanent, can be blocked by dependencies, and may continue after the initial request. An erased occurrence cannot be replayed with its previous ID. Review the operation's completion before treating cleanup as finished. --- # Forms Source: https://docs.maxclicks.ai/forms # Forms Collect signups, their details and their email preferences in one place. A published form creates or updates a [contact](/contacts); that contact can then join an audience or start a [workflow](/workflows). For example, a newsletter form can collect a name and email, add a `newsletter-signup` tag, and subscribe people to your product updates when they opt in. Your welcome workflow responds to the resulting contact change. The path is **form submission → contact saved → matching audience or workflow**. With double opt-in, an opted-in submission waits for confirmation before the contact is saved. Follow [Create a signup form](/guides/forms) for a first setup. ## Choose what it collects Open **Forms → Create form**. In the **Builder**, choose a **Contact Schema**, then add the attributes you want to collect. Mark essential fields **Required**. Changing the schema removes custom fields that do not exist in the new one. Email is always required. Collect either full name or first and last name, not both. A field's type and validation come from its [attribute](/attributes). Edit that attribute to change what it accepts. Set a title, optional message and **Submit Button Label** under **Form Content**. The adjacent preview lets you switch between the form, teaser and success screen before publishing. ### What a submission changes The form accepts only its configured writable fields. Unknown fields are ignored; evaluated attributes, whether On demand or Scheduled, cannot be submitted. Required values are validated against the contact schema. Forms match email-only contacts within the selected schema. A contact identified by a product user ID can exist at the same address; sharing an email does not automatically merge those identities. See [contact identity](/contacts). The form adds its tags and preserves existing ones. When optional consent is declined, a new contact remains unsubscribed and an existing contact keeps their prior subscriptions and topics. Declining is not an unsubscribe action. Configured AI auto-fill can use credits. The contact write can trigger workflows and outgoing webhooks, so test the complete signup before sharing the form widely. ## Ask for marketing consent Choose one **Email Marketing Consent** option: * **On form submission:** submitting subscribes the person. Explain that subscription in your form copy. * **On form submission if the box is checked:** subscribe only when the visitor ticks the checkbox. Set its **Consent Checkbox Label**. * **Don't collect marketing consent:** collect the record without adding an email subscription. **Communication Topics** adds your selected public topics only when the visitor opts in. Visitors do not select these topics themselves, so describe what they will receive. **Consent Disclaimer** adds a notice below the button. Recorded consent appears in the contact's audit trail. ## Confirm the address before subscribing Turn on **Enable Double Opt-In** to require a confirmation link for opted-in submissions: 1. The submitted details wait for confirmation. 2. maxclicks sends a confirmation email. 3. The person opens its single-use link within 24 hours. The contact is saved and the confirmed consent is recorded. The confirmation uses a maxclicks sending address and works before you verify your domain. A welcome email or workflow sent from your workspace still needs a [verified domain](/domain). Double opt-in uses fixed **Please, check your email** and **Subscription confirmed** screens. Custom thank-you content and redirects do not apply. If a visitor leaves an optional consent checkbox unchecked, no confirmation email is sent and their details are saved immediately. The hosted form still displays its check-email screen in this case. Test both consent choices so you understand the experience. Confirmation emails are limited to two per address per form per hour, and 200 per form per hour. ## Choose how it appears **Display Mode** offers an **Inline form** inside your page or a **Popup form** over it. For a popup, configure: * **Teaser:** the invitation to open the form, shown before opening, after closing, both or never. * **Popup Triggers:** delay, exit intent or scroll percentage, with a frequency of always, once, once per session or once per day. * **Close Behavior:** the close button, overlay click and Escape key. Choose a floating position or a centered overlay, then its background color and opacity. **Image** places artwork above, beside or behind the form. ### Popup timing and frequency Choose triggers for when the invitation appears. A positive delay waits that many seconds. Exit intent responds when the mouse leaves the top of the page; do not rely on it as a mobile gesture. Scroll opens the invitation when the configured percentage is reached. Multiple triggers are alternatives: the first qualifying one opens the invitation, rather than waiting for all of them. When a teaser is configured to appear first, a trigger displays the teaser. The visitor then opens the form from it. **Show first and if the customer closes the form** brings the teaser back after closing; **Show if the customer closes the form without subscribing** starts with the form and shows a teaser afterwards. Disabling the close button is separate from disabling Escape and overlay-click dismissal. | Frequency | What it measures | | ---------------- | ----------------------------------------------------------- | | Always | Each eligible page display can open the invitation | | Once | A previous display recorded in the browser prevents another | | Once per session | A display recorded for the browser session prevents another | | Once per day | At least 24 hours must pass since the stored display time | Frequency is browser-based, not a customer identity rule. Another browser or cleared storage can show the popup again. The current popup display history is shared across forms rendered on the same origin, so testing a second form can encounter a frequency limit from an earlier form. The builder preview bypasses frequency; verify the published embed in a fresh browser session when testing timing. ## Say thanks, or send them somewhere With double opt-in off, **After Submission: Thank You Page** lets you show a title, message and optional coupon code, or redirect to a URL. With double opt-in on, its fixed confirmation screens take over. ## Keep out the bots Every form has built-in rate limiting. **Form Protection** adds Cloudflare Turnstile, Google reCAPTCHA v3 or hCaptcha. Save the provider keys in **Settings → Forms Protection**, then select it in the form. Configuring keys requires an Admin with editing access. Selecting a provider without valid keys prevents submissions. A custom form must integrate the configured provider and submit a valid `captchaToken`. The hosted form and embed handle this for you. Keep the provider's secret key on the server. ## Brand it Under **Branding**, choose your brand, [brand kit](/brandkits), palette and style pack. Check the preview after choosing fields and consent text. ## Publish and share it Click **Publish**. Until publication, the hosted link, embed and submission endpoint return `404` and accept no submissions. **Share** offers three options: * **Hosted link:** share a ready-to-use page. * **Embed it on your website:** copy both the iframe and generated script. A popup needs the script to open and respond to scroll or exit intent. * **Build your own form against the API:** use the generated example with your configured field names. **Edit** unpublishes the form and returns to the Builder. The live form is unavailable until you publish again; plan changes around that interruption. ## See how it is doing **Analytics** shows views, submissions, conversion rate and a daily trend. Successful repeat submissions count too, so the submission total is not a count of unique new contacts or new marketing subscribers. Opted-in submissions awaiting double opt-in are counted after confirmation. **Conversion Rate** divides submissions by unique views when that count is available, otherwise by total views. It rounds to a whole percentage and caps the displayed result at 100%. For example, 12 submissions and 100 unique views show 12%, even if some submissions update existing contacts. Confirm subscription status on the contact before treating submissions as new subscribers. The trend compares the latest seven days of recorded submissions with the preceding seven. It needs at least 14 daily entries and a nonzero previous total. A missing comparison is not evidence of zero growth. The viewed-only chart uses total views, so its denominator can differ from the conversion-rate card. Test a new address, an existing contact, and both consent choices. Inspect the saved fields, tags and subscriptions on each contact. The builder preview alone does not test submission or downstream automation. ## Diagnose a form that does not complete | What you see | What to inspect | | --------------------------------------------- | ---------------------------------------------------------------------------------------------- | | The hosted form returns 404 | Confirm the form is published; clicking Edit takes it offline until the next Publish | | A popup does not appear | Check the generated embed script, triggers, teaser mode and browser frequency history | | A submission rejects a field | Check required fields and the selected schema's stored attribute definitions | | CAPTCHA blocks the submission | Check the selected provider and its saved credentials; a custom form must supply a valid token | | The visitor cannot find their contact | Check the selected schema, email-only identity matching and pending double opt-in | | A contact exists but no welcome email arrives | Inspect consent, sender verification and the actual workflow trigger/run | Test on the site where you deploy the form, using the exact embed from **Share**. A builder preview establishes appearance; a completed signup establishes the saved contact and any downstream work. ## Start a workflow from a submission Use **Record Created or Updated** on the form's contact schema. There is no separate form-submission trigger. For the newsletter example, narrow the condition to the form's `newsletter-signup` tag and the required subscription. An unfiltered trigger responds to other contact updates too. A contact already matching the condition may not start another run on a repeat submission; this is not a trigger for every form submission. With double opt-in, an opted-in contact is written after confirmation. Inspect the resulting workflow run before relying on the welcome email. See [workflows](/workflows). ## Related * [Create a signup form](/guides/forms) * [Contacts and identity](/contacts) * [Fields and attributes](/attributes) * [API reference](/api) --- # Guides Source: https://docs.maxclicks.ai/guides # Guides Pick a customer outcome and build the campaign behind it. Follow the data into the audience, the message and the checks before sending. New here? [Create your first personalized email](/guides/first-personalized-email). You will compare three customers' messages before setting up a live send. ## Choose the outcome [Make one email personal](/guides/first-personalized-email): Supply three customer profiles. Mark one passage for Adaptyle. Compare the finished messages. [Activate new signups](/guides/workflows): Wait two days, check for a first project and send a reminder only while it is still needed. [Encourage a repeat purchase](/guides/repeat-purchase): Select returning buyers from order history and adapt a product introduction to their preferences. [Send your first campaign](/quickstart/send-your-first-email): Verify your sender, review recipients, send an inbox test and inspect delivery results. ## Popular tasks | Work you are doing | Guide | | ------------------------------ | ---------------------------------------------------------------------------------------------------------------- | | Bring in customer context | [Add contacts](/guides/contacts), [import a list](/guides/import), or [connect related data](/guides/data). | | Choose who receives a campaign | [Build an audience](/guides/segments) and inspect the matching records. | | Create content with your brand | [Set up your brand](/guides/brand), [design an email](/guides/design), then [add Adaptyle](/guides/personalize). | | Direct your agent | [Brief maxinja](/guides/maxinja) and review its saved work. | | Launch and operate | [Send a broadcast](/guides/broadcast), [read results](/guides/reports), and [check usage](/guides/billing). | | Connect sending to your app | [Create a reusable template](/guides/templates), then follow the [developer quickstart](/quickstart). | ## How these guides work Each walkthrough starts with one job, names the data you need, and explains what to inspect before putting the result into use. References hold the full settings, costs and recovery details. For help choosing between maxinja, Adaptyle, evaluated attributes and workflow steps, read [Choose the right tool](/guides/choose-your-tools). To learn the product in order, start with [maxclicks 101](/101). --- # Manage billing Source: https://docs.maxclicks.ai/guides/billing # Manage billing Admins manage billing separately for each workspace. Use this guide to find your plan, check spending and manage payments. For prices and allowances, read [How billing works](/billing). ## Find your plan and balance Open the space menu and choose **Billing**, or open it through **Settings**. **Billing & Usage** has two tabs: * **Overview:** your plan, email allowance, credit balance, payment methods and invoices. * **Usage:** recorded credit spending by period, category and member. Both tabs are available on Free and Premium. If you cannot see Billing, ask a workspace admin. The subscription and prepaid credits are separate. Premium covers the workspace and reserved record capacity, with no per-seat charge. AI work and email above the allowance use the shared wallet. No monthly AI allowance or credit gift is included. Buying credits on Free leaves its 100-recipient monthly cap unchanged. See [AI and usage credits](/billing/credits). ## Top up, and keep it topped up 1. On **Overview**, add a card under **Payment methods** if needed. 2. Click **Add Credits** and enter an amount from 10 to 10,000 in your workspace currency. 3. Confirm the charge and complete any bank verification. Check that **Credits Balance** updates. To refill automatically, choose **Enable Auto Reload**. Set a reload amount and the balance that triggers it, then save. **Edit auto reload** changes or disables it later. Auto reload checks about every 15 minutes. Leave enough credit for work between checks; it is not an instant refill. The [auto reload reference](/billing/credits#automatic-reload) explains the limits and an example charge. ## See where it went Open **Usage**, choose a date range and group by category or member. Use this to identify which type of work or member drove spending. The report does not attribute charges to individual campaigns. Change a filter or reload the tab to see recent spending. On **Overview**, open an invoice or download its PDF. Invoices show subscription charges and credit purchases; **Usage** shows how credits were spent. ## Upgrade, change or cancel Choose **Upgrade to Premium** to review monthly pricing. New subscriptions are monthly only. Check the [plan calculation and capacity changes](/billing) before confirming. To cancel, choose **Cancel subscription** on the plan card. Premium stays active until the paid period ends. Choose **Resume subscription** before then to undo the cancellation. After it ends, the workspace returns to Free and keeps purchased credits. ## Plan for work already running All teammates, campaigns and workflows use the same prepaid workspace wallet for paid work. Before a large send, review the composer's **Estimate** and fund the expected usage. If work stops, inspect its reason before retrying. Waiting for credits, a terminal failure and an exhausted email allowance need different responses. Cancelling Premium or disabling auto reload does not stop automation. Use [broadcast stop](/broadcast#cancel-or-stop-a-send) or [workflow pause](/workflows/manage#pause-it-without-losing-anything) to prevent further execution. Continue with [How billing works](/billing) or [AI and usage credits](/billing/credits). --- # Set up your brand Source: https://docs.maxclicks.ai/guides/brand # Set up your brand Give your agents the design and voice guidance your team already works with. A brand kit holds reusable colors, fonts, imagery, social links and writing rules. Choose a published kit for each email or form; business identity and address live separately in Settings. ### Open your brand Your brand lives in two places. For your brand name, logo, website, and address, open the space menu in the top bar, go to **Settings**, then **Brand**. For the colors, fonts, assets, and social links your emails use, open **Brand Kits** in the top bar. Review the kit created during setup if one is available, or click **Create brand kit** to make one. ### Add your logo and details In **Settings**, **Brand**, upload your logo and, if you have one, a small icon. Fill in your brand name and website. Your business address goes here too; maxclicks uses it in the sending footer. ### Set your colors In your brand kit, open **Color Palettes** and choose the colors your emails use: background, text and headings, buttons, and links. Pick colors that match your website so your emails feel familiar. ### Pick your fonts and spacing Open **Style Packs**. Choose a font for your headings and one for your body text, then set corner rounding and spacing. Templates using that style pack inherit these choices where their design uses the kit's styles. ### Add your social links Open **Content & Socials** and add the profiles you want in your email footers, like Instagram, LinkedIn, or YouTube. Only the ones you add show up. ### Add your images and voice (optional) **Assets** is where you keep images you reuse in emails. **Brand Voice** is where you describe your values and tone as guidance for maxinja. Enter the voice manually, then inspect generated copy against your rules. ### Publish your brand kit When it looks right, **publish** the brand kit. Publishing is what makes it available to your emails and forms, and the first kit you publish becomes your brand default. After that the kit stays live: saved edits affect work that reads the kit again. They do not rewrite existing prose, sent emails or frozen broadcast content. Use a second kit when experimenting. A new brand kit starts from a default color palette and style pack, so you have something working from the first minute. Adjust them to match your website, then publish when it looks right. ## Check the kit in actual content Open a draft email and select the kit, palette, style pack and asset collection. Review the footer, links, typography and a generated paragraph with real sample data. Writing rules guide AI generation; they are not an automatic banned-word or compliance check. Keep required wording outside AI regions. An existing hard-coded color does not become a brand reference merely because you changed kits. Ask maxinja to update the draft where needed, then inspect a fresh preview. See [brand kits](/brandkits) for publication and deletion behavior. ## Next With your brand ready, [design an email](/guides/design), select the kit and inspect how its colors, typography and voice appear in the finished message. --- # Send a broadcast Source: https://docs.maxclicks.ai/guides/broadcast # Send a broadcast Send a new-roast announcement to recent coffee customers. You will finish with a named broadcast, a checked audience, recipient previews and an explicit choice to send now, schedule or save a draft. The Halden images below show a real saved demo campaign and its generated previews. They demonstrate the review surfaces; they do not show a delivered campaign. To follow the same job in your workspace, use your own product facts and customer records. Before starting, have a customer [contact schema](/contacts), purchase data linked to those customers, a [verified sender](/guides/domain), and a published [brand kit](/guides/brand). Confirm marketing subscriptions and any topic you plan to use. If purchase data is not connected yet, build a [simpler saved segment](/guides/segments) using fields you can verify. ### Start a new broadcast Open **Broadcasts** and choose **New broadcast**. The first screen asks “Who is this broadcast for?”. Choose the customer schema; the next step is the audience below. After creation, rename the untitled campaign in its header to “New roast — recent customers”. ### Choose who receives it Select the **Contact schema** for customers and a saved [segment](/guides/segments), or choose **New segment**. For this example, describe “customers with a recorded coffee purchase in the last 30 days”. Review the generated rule: it must match each customer's own purchase, with the intended date boundary. Check a customer with a qualifying purchase and one whose last purchase falls outside the window. A customer who qualifies but has unsubscribed can still match the audience rule; marketing eligibility is checked separately. The saved Halden example below uses its own custom audience conditions. ### Design the email Choose **Next** from the audience screen. On “Choose your email’s starting place”, select **Dynamic Composer** and **Marketing**, then **Next** to open **Composer**. The mode is fixed on the email. If you are opening an existing broadcast without content, use **Add content** instead. The broadcast supplies one audience contact as **To**. In **Data**, expand that contact and select the purchase fields or relationships the message will use. Check the actual key shown there before inserting Liquid. Add the verified sender, subject, product description and a button to the product page in the [editor](/guides/design). Select your [brand kit](/guides/brand). Keep product facts, prices and links fixed; use [Adaptyle](/adaptyle) only for the introduction that connects the roast to recorded preferences. Ask maxinja to leave a general introduction when purchase context is missing. ### Preview and send a test Inspect a customer with relevant purchase history, then a sparse sample with no useful preference. Check that the first introduction uses the supplied facts and the second stays general. Generate a fresh personalized preview after changing the sample. Use the toolbar’s **Send test** to mail that rendered sample to yourself. The preview enables the control; the send still needs a sender on a verified [domain](/guides/domain), available allowance and an eligible test address. It uses email allowance and checks inbox appearance. It does not send to the sample customer or prove the campaign audience is correct. ### Send now or schedule In **Composer**, review **Checks** and **Personalize → Estimate**. Open **Send** (or **Schedule** after saving a draft), review the audience, then choose **Send now**, **Schedule for later** or **Save as draft**. Marketing requires email and applicable topic consent; suppressions are checked before dispatch. The matching audience count is not the final number of deliveries. Ask maxinja: "Prepare a draft campaign for customers who bought in the last 30 days. Explain the audience rule, use our brand kit, keep the sale dates and discount exact, and leave it unscheduled for review." Inspect the resulting selection and messages before committing the send. ## Review before you commit 1. Inspect the audience rule and several matching contacts. Confirm that the purchase or behavior belongs to this customer. 2. Check **Data**, **Personalize** and **Checks** in the email rail. Preview sparse and complete customer examples; confirm sender, links, language, topic and fixed offer facts. 3. Read **Personalize → Estimate**, including generation, corrections and reviews. Unknown amounts are not zero; an uncalibrated duration is not a completion promise. 4. Confirm the wallet, email allowance and domain are ready for the planned work. 5. Check the schedule in the app's displayed time zone. Preparation may begin early; sending cannot begin before the scheduled dispatch time. Preparation fixes the recipient selection and campaign revision. Later segment membership changes do not add people to that campaign. A successful preview is a sample; campaign checks still apply to the generated work. ## Change your mind after scheduling Open **Metrics** and read the sending banner. **Cancel send** is available before preparation starts and returns the campaign to editing. After preparation begins, **Stop sending** cancels remaining work permanently; messages already being dispatched may arrive. It cannot recall mail or undo usage already incurred. A credit wait or quality hold is different from a stopped campaign. Inspect the reason before creating another send. The [broadcast reference](/broadcast#retry-eligible-failed-recipients) explains the **Retry** action for eligible failed recipients and **Close remaining work** after a campaign finishes. Previously accepted and uncertain deliveries are protected from that retry. ## If preparation needs attention A count waiting for its audience filter means the filter is not ready; it does not mean zero customers. An error in **Checks** needs correction before scheduling. A campaign waiting for credits needs a billing review. A rejected generated email belongs in **Quality review**, where you can inspect its exact wording and source information. Follow [Review a quality hold](/broadcast#review-a-quality-hold) for eligible human approvals and their limits. ## After you send Start with **Emails** to check recipient-level preparation and sending outcomes, then **Metrics** for delivery and response. An individual email’s Sent status means provider acceptance; Delivered arrives later and does not prove inbox placement. The aggregate funnel’s Sent count includes linked email records, including pending or failed records; use [report definitions](/guides/reports#read-the-rate-and-its-denominator) before comparing totals. Check failures and cancellations before deciding a customer needs another message. Learn how to read these in [See your results](/guides/reports). --- # Choose the right tool Source: https://docs.maxclicks.ai/guides/choose-your-tools # Choose the right tool Start with the work you need done. Writing a campaign, finding a customer's latest order and adapting an email are different jobs. Here is where each belongs. ## Match the tool to the job | You want to… | Use | What you get | | -------------------------------------------------------------------- | --------------------------------------------------------- | --------------------------------------------------------------------------------------------- | | Build or change a campaign from a brief | [maxinja](/maxinja) | Saved audiences, content and workflow configuration that you can open and review. | | Reuse a customer fact from related records | [Evaluated attributes](/evaluated-attributes) | A saved definition, such as this customer's latest completed order, evaluated when requested. | | Check current records at a particular point in a journey | [Query](/workflows/blocks#query) | A lookup result the following steps can use. | | Interpret customer context or make a recommendation during a journey | [AI step](/workflows/ai#the-ai-step-always-calls-a-model) | A model-generated answer with a defined output shape. | | Adapt a passage in each recipient's email | [Adaptyle](/adaptyle) | Generated wording inside the template you designed. | | Insert an exact value or show content by a rule | [Liquid](/liquid) | A name, order number, price or conditional passage based on supplied data. | ## Follow one onboarding journey Suppose you want signups to create their first project. A teammate creating a project should not count as the signup's own progress. 1. **Brief maxinja.** Ask it to build a signup journey with a two-day wait and a first-project email. Keep the journey unpublished while you review it. 2. **Define the customer fact.** An evaluated attribute can count projects owned by that person. Review the ownership match against the actual user and project records. 3. **Check after the wait.** A Query step can read the person's current projects. Branch on the result so someone who already started leaves before the reminder. 4. **Adapt the message.** In the email, Adaptyle uses the person's goal and selected project context to suggest a first project. The button and destination stay authored in the template. The Query result answers whether to send. Adaptyle answers what the marked passage should say. You do not need an AI step just to count projects or personalize that passage. Follow [the onboarding walkthrough](/guides/workflows) to build and inspect this journey. ## When an AI step earns its place Use an AI step when its answer needs to affect the journey beyond one email. For example, interpret a customer's free-text onboarding goal and return a recommended onboarding path. A later branch can choose the path, and an email can use the same answer. Provide the evidence, define the expected fields, and test how the next step handles an empty or unusable answer. For a fixed rule such as “project count is zero,” use saved logic instead of asking a model to decide on every run. [Workflow AI](/workflows/ai) explains the execution modes and costs. The AI answer belongs to the run. Save it with **Create or Update Records** if another campaign needs it on the customer's profile. Include any required writing guidance in the AI prompt or selected data; the later email's brand kit does not automatically rewrite a Liquid-inserted answer. A terminal generation failure stops the run, so a generic answer requested for missing data is not a recovery path for a failed model call. ## Choose how a value stays current An **evaluated attribute** has **On demand** or **Scheduled** timing. On demand runs saved lookup logic when requested; it does not ask a model to invent a value each time. Scheduled publishes complete Event-history calculations on its configured cadence. Check its publication status and **Data through** timestamp before treating a Scheduled result as current. A **Query step** reads at its position in the journey. Data collected before a wait does not automatically become a fresh lookup afterward. Published calculation snapshots can also remain fixed for an active run. See [data expansion](/attribute-expansion) for those boundaries. ## Start with the smallest useful build For a personal campaign, start with one email, the fields it needs and [three recipient previews](/guides/first-personalized-email). Add a journey when timing or follow-up matters. Add an AI step when its judgment changes what the journey does. --- # Add your contacts Source: https://docs.maxclicks.ai/guides/contacts # Add your contacts Give a campaign the customer details it needs, together with the preferences that determine whether you can send. This guide covers creating a contact, checking its fields and maintaining its communication preferences. For a full list, use [CSV import](/guides/import). Keep one record for the person within your chosen contact schema. Sarah's email and preferences belong here; each of her orders belongs in an [Order object schema](/objects). The order fields shown below come from evaluated lookups over those related records, so adding Sarah's profile alone will not populate her purchase history. ### Open your contacts In the top navigation, open **Records** and pick your contact type, for example Customers. That is the list of records in the selected schema; delivery eligibility is checked separately. In a new space it is empty. ### Add a new contact Click **Add**, followed by your contact type's name, for example **Add Customer**. If you have no contacts yet, the button on the empty page reads **Create Customer** instead. At a minimum, add the person's email address, since that is how you reach them. Add their first and last name too, so your emails can greet them properly. ### Fill in more details Add anything else you know: phone number, notes, and tags. Tags are short labels like "vip" or "newsletter" that you filter and group by later. ### Set their email consent **Communication topics**, at the top of the form, is the only consent control here, and it decides whether this person can be sent marketing email at all. Pick at least one topic and the contact is created subscribed to email. Leave it empty and the contact is created **unsubscribed**, so every marketing broadcast skips them. Defaults vary by input path. The standard CSV importer also starts with default subscription turned off. The general record API can subscribe a newly created contact when `subscriptions` is omitted, so send an explicit empty list when that is intended. Review forms and workflow subscription settings separately. ### Create the contact Click **Create**, followed by your contact type's name, for example **Create Customer**. The person appears in your contacts list right away. Matching uses the identifiers on the record. Inspect the saved contact and resolve any conflicting identifier reported by the app; see [contact identity](/contacts#how-maxclicks-knows-two-records-are-the-same-person). That update includes consent. Putting an existing subscriber through this form with no topic selected replaces their subscription with an empty one, and they stop receiving marketing email. To change details on someone you already have, open them from the list and edit there instead: the edit form does not touch subscriptions. ### Edit a contact later To change anything, open a contact from the list, update their details, and click **Apply**. Your edits take effect immediately. ## Verify the profile before using it Confirm the selected schema, stable User ID and intended email. Matching stays within a schema and gives User ID precedence over email. It does not merge separate profiles across schemas. Read any conflict rather than submitting the same person repeatedly with new identifiers. Check the stored values that matter to your first audience. For evaluated fields, use **Compute value** on a real record; for a scheduled evaluated attribute, inspect its published **Data through** timestamp. A filled preview value does not prove that the source supplied every required fact or that a background calculation is current. ## A note on permission A marketing broadcast reaches only the contacts subscribed to email, and to its topic if it has one. Transactional email has different subscription checks but can still be blocked by bounce suppressions or fail delivery; it is not a guarantee that every address receives the message. Have permission before you add someone by hand. If a contact unsubscribes later, maxclicks records it and stops sending them marketing email. You do not have to track that yourself. To subscribe people you added without a topic, select them in the contacts list and click **Subscribe**. Once your contacts are in, ask maxinja to put them to work. Try: "Write a welcome email to my new contacts." maxinja drafts the email and can pick who receives it. ## Next Learn how to [build an audience](/guides/segments) by grouping contacts for a campaign. --- # Understand your data Source: https://docs.maxclicks.ai/guides/data # Understand your data Start with what a campaign needs to know. A renewal date can decide when to write; a purchase can explain what to say. This guide shows how to find your records, define their fields and make that context usable across audiences, email and workflows. ## The three kinds of records **In a software business:** Maya is the **contact**, Studio North's account and its projects are **objects**, and “Maya created a project yesterday” is an **event**. **In a store:** Sarah is the **contact**, each of her orders and the products she buys are **objects**, and “Sarah completed a purchase yesterday” is an **event**. Maya and Daniel can share an account without sharing the same project history. Sarah can have four orders without becoming four contacts. Store stable identifiers, then define [evaluated relationships](/101/data-model/relationships) that connect each person to the right objects. Those relationships give a segment its evidence and an email its context. The difference that matters day to day: you keep contacts and objects up to date over time, but an event is a snapshot. Its accepted facts are not edited into a later occurrence, and a configured [workflow](/guides/workflows) can act when the event is ready. Events are also the one kind that does not appear under **Records**. To read the ones that have fired, open **Settings**, then **Events**, click the event type, and use its **Logs** tab. ## Look at what you already have ### Open your records In the top navigation, open **Records**. The dropdown lists your contact and object types. ### Open a record type Click one, like your customers, to see the records inside it and the fields each one carries. ### Open a single record Click any record to see everything about it in one place. This is where you edit its fields or add notes. Every contact type arrives with a set of built-in fields you cannot remove: name, email, phone, tags, notes and a few others. They are listed on the [contacts reference](/contacts). ## Add a field of your own Anything your business tracks that is not already there, you add yourself: a plan level, a birthday, a favorite product. ### Open the type's fields Go to **Settings**, then **Schemas**, click the type, and switch to the **Attributes** tab. Business event fields are on **Attributes** under **Settings → Events**. ### Create the field Click **Create attribute**, choose **Stored value** under **How is the value set?**, and fill in the field details. `Value type` What the field holds: Text, Number, Decimal, Checkbox, Date, Email, URL, Multi-select and more. The type is the validation rule, so a bad value is rejected whether it came from a CSV or from your app. `Name` The label people see in the app, like `Loyalty points`. `Key` The name you use in an email and over the API, like `loyaltyPoints`. maxclicks suggests one from the name. `Description` Optional, but write it. maxinja reads it every time it builds a segment, an evaluated attribute or workflow logic that touches this field. The full list of types and their settings is on the [attributes reference](/attributes). ### Say what happens when it arrives empty Choose **When a record is created without a value**: **Leave it empty**, **Use a default value**, **Require it**, or **AI autofill**. AI autofill can infer from the record or follow your prompt. These policies apply to new records; they do not fill old records or rerun on an update. AI autofill consumes credits for each new record and missing AI field that needs generation. Import rows that match existing records do not regenerate those fields. Use a fixed default for a fixed answer and test a few new records before a large import. ## Fields maxclicks works out for you Choose **Evaluated attribute** under **How is the value set?** and you describe the answer instead of filling the field in: "the company this contact works for", "total spent on completed orders". Review the generated timing. An on-demand definition runs whenever a consumer requests it, using source records available at that moment. A scheduled event-history definition instead reads a published background result. Review the generated target and **one** or **many** cardinality. Test a record with a known match and one without a match. See [evaluated attributes](/evaluated-attributes). For a recurring measure of event history, create an **Evaluated attribute** on the destination contact or object schema. Choose whose profile receives the result, then describe the source, count/sum/latest measure, window and cadence. Review the result and check the published **Data through** timestamp. A valid publication with no matching history is empty, not zero. No complete publication or an expired enabled result blocks a new decision that requests it. See [scheduled evaluated attributes](/attributes#computed-attributes). Both kinds need [expansion](/attribute-expansion) when selected as email or workflow data. A background calculation is not rerun merely because the contact is opened. ## What you can change later, and what you cannot Rename a field, rewrite its description and adjust its settings whenever you like: open the **Attributes** tab and click the row. The **Key** stays fixed. Change a stored **Value type** or validation rule through setup review, choosing what happens to **Existing records**. Inspect converted examples and affected saved rules in **Changes**. Use [the attribute change reference](/attributes#what-you-can-change-later-and-what-you-cannot) before applying a change to populated fields. Deleting a field only works while nothing depends on it. If another field, a segment, a broadcast, a workflow step, a webhook, an email template or a form uses it, maxclicks refuses and names what is in the way. Unhook those first. For a populated schema, follow [Setup changes](/setup-changes) to inspect converted values, affected campaigns and application status. ## Next Head to [Add your contacts](/guides/contacts) to fill your record types, then [build an audience](/guides/segments) from them. --- # Design an email Source: https://docs.maxclicks.ai/guides/design # Design an email Build a product announcement with a short introduction and one button to its product page. You will finish with a branded draft and a rendered preview you can inspect before sending. Have your product facts, destination URL, published [brand kit](/guides/brand) and a sample customer ready. This guide uses the visual editors; if your team brings finished markup, open the [HTML](/editor/html-code) or [MJML](/editor/mjml-code) reference. ### Open the editor Open the email you are working on, from a [broadcast](/guides/broadcast), a template, or a [workflow](/guides/workflows) step, and click into its content. Click **Preview** in the editor toolbar to show your email beside the editor, or over it when the window is narrow. ### Add your content Add blocks one at a time: **Text** for your message, **Image** for a photo or banner, **Button** for a link like "Shop now", and **Divider** or **Spacer** to give things room to breathe. You can also paste a URL into a **YouTube Video**, **X Post**, or **Link Preview** block. Click a block to edit it, or drag it to move it. ### Choose a layout In the Dynamic Composer, arrange your content into sections, and split a section into columns when you want two things side by side, like an image next to some text. Reorder sections by dragging them until the flow feels right. The Plain Composer has no sections or columns: it runs down one column, which is what gives it a written-by-a-human look. ### Make it look like your brand Select your published [brand kit](/guides/brand) and its palette, style pack and assets in email setup. References use that branding when rendered; explicit block styles remain overrides. A campaign already preparing keeps its captured brand settings, and an already sent email does not change. ### Preview and adjust Open the preview to inspect the rendered subject, content and layout. It sits beside the editor when the window is wide enough, and covers it when it is not. If your email has [AI-personalized](/guides/personalize) parts, choose a **Model** and a **Thinking** depth under **Personalize → Runs with**, then click **Generate preview** to render one person's version. Tweak the wording, swap an image, or nudge the spacing until you are happy. Ask maxinja: "Design a welcome email with a hero image, a short intro, and a button to our homepage." It builds the layout and writes the copy, and you can keep refining from there. ## Review more than the canvas Use **Design** to edit the selected block and the maxinja input at the bottom of the right panel to request a specific change. **Personalize** lists prompts and variables, flags missing fallbacks, and holds the runtime model and estimate. **Data** controls the sample and recipient. **Checks** lists readiness problems. Switch between **Base** and **Personalized** to compare fixed content with generated copy. Test a recipient with complete context and one with missing values. Use **Send test** for inbox appearance; it sends the rendered sample and does not establish marketing eligibility or audience correctness. Before scaling up, read **Personalize → Estimate** and its [assumptions](/editor#read-the-estimate-before-scaling-up). An unknown campaign cost is not zero, and the editor’s **Checks** do not replace broadcast preparation checks. ## Related Once your email looks right, learn how to [send a broadcast](/guides/broadcast) to a group, or [set your brand](/guides/brand) so every email starts on-brand. --- # Connect your domain Source: https://docs.maxclicks.ai/guides/domain # Connect your domain Establish the sender your customers will recognize. Verify a domain your business controls by adding the DNS records maxclicks supplies, then use it for your sender profiles. This guide covers that setup and where to check its status. Broadcasts, templates, workflow emails and editor test sends need your own verified sending domain. An in-app preview is separate from an inbox test. Form double-opt-in confirmation uses a platform address; that does not provide a fallback sender for your campaigns. Before starting, make sure you can edit this workspace and publish DNS records for your domain. For an example sender `hello@mail.example.com`, you will add `mail.example.com` as the sending domain and create the `hello` sender profile after verification. The DNS values must come from your own maxclicks setup, not from an example screenshot. ### Find your domain Open the space menu in the top bar, go to **Settings**, then **Domains**. If you named a sending domain during setup it is already listed, so select it. Otherwise click **Add domain**, type the domain you send from, such as "mail.yourcompany.com", and confirm. ### Copy the DNS records maxclicks shows a short list of records to add to your domain: they prove you own it and sign the mail you send from it. Keep this page open, you will copy each value in a moment. ### Add the records at your DNS provider Sign in wherever you manage your domain (GoDaddy, Namecheap, or Cloudflare, for example) and find the DNS settings. Add each record from maxclicks, copying the name and value exactly. This is the one step that happens outside maxclicks. ### Verify Come back to maxclicks, open the menu on your domain's row, and click **Verify**, then **Start verification check**. The **Status** column beside each record shows which ones maxclicks has found. The button starts the verification process. While the check is running, the button is disabled. A domain can look correct in your provider's dashboard before maxclicks observes the published records, so follow each record's status instead of repeatedly adding the domain. DNS caches and provider processing can add delay. Compare the exact names, types and values supplied by maxclicks, then allow the records to become visible to verification. Read the overall **Verified For Sending** state before testing a real send. ### Ready to send The first domain becomes the workspace default when added. Verification makes it usable; choose a sender profile or From address on the actual email, then use **Send test**. Existing emails can retain an explicit sender instead of following the new default. For a record you are stuck on, ask maxinja: "What does a DKIM record do, and where does it go at my DNS provider?" It explains each record in plain terms. To see how far along you are, check the **Status** column in the verification dialog. ## Good to know The same background check can also take verification away. If you remove the records at your provider, maxclicks marks the domain unverified again and every send from it stops. Leave them in place. First domain verification grants no credits. Eligible new workspaces can receive platform-funded maxinja setup assistance before their first domain verifies; ordinary generation and previews still need available credits. See [Plans and billing](/guides/billing). ## Next With your domain connected, add a friendly sender name under **Settings**, **Sender Profiles**, then [send your first broadcast](/guides/broadcast). --- # Create your first personalized email Source: https://docs.maxclicks.ai/guides/first-personalized-email # Create your first personalized email Build one email that gives three customers different advice. You will choose the customer context, mark a passage for Adaptyle and compare the finished messages before sending. ## What you will make Loopwell is a fictional project app. Three signups have not created a project, but they have different work ahead: a launch, customer research and a client handoff. The same email can suggest a relevant project to each person. Maya's preview proposes launch sign-off without duplicating her teammate's asset work. Leila's uses her eight interview notes. Hugo's focuses on tomorrow's handoff. The design and **Create your first project** button stay the same. These are saved generated examples; a new generation can use different wording. ## Before you begin * A [workspace](/guides/setup) with a contact type and permission to edit email content. * Three test contacts, or the sample records below. Keep them separate from a live audience. * Available AI credits for previews and a published [brand kit](/guides/brand) if you want to use your brand guidance. An in-app preview does not send an email. An inbox test additionally needs a [verified sending domain](/guides/domain) and email allowance. ## Add three customer profiles Open **Records**, choose your contact type, and use **Add Customer** or the equivalent button for that type. Use distinct test email addresses you control, enter the following names and put the context in **Notes**. This small example uses a built-in text field; a production campaign can use your selected profile fields and related records instead. | Customer | Notes | | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | Maya Nolan | No projects of her own. Launch in 10 days. Daniel owns launch assets. Sales and design sign-off is still missing. | | Leila Chen | No projects of her own. Eight customer interviews completed. Wants to prioritize the next product sprint. A teammate owns participant recruitment. | | Hugo Meyer | No projects of his own. Client handoff tomorrow. Has a draft outline and one outstanding client decision. | You can also [download the sample CSV](/examples/first-personalized-email.csv) and use **Import Customers**. The file uses the built-in keys `firstName`, `lastName`, `email` and `notes`. Its example addresses are for previews only. Leave **Subscribe to email by default** off and inspect each saved record after importing. See [CSV import](/guides/import) if your file uses different columns. ## Create the shared email Open **Broadcasts → New broadcast**. Choose your test contact type and audience, then select **Next**. Choose **Dynamic Composer** and **Marketing**, then **Next** to create the draft and open **Composer**. Rename it **First project preview** from its header. If you are opening an existing draft with no email, use **Add content**. Give maxinja this brief in the composer, referring to the contact type you chose: > Draft a first-project email for Loopwell with the subject “Your first Loopwell project”. Use a simple layout, a greeting and one paragraph of advice. Mark that paragraph for Adaptyle. Use the selected customer's notes to suggest a project. Keep the button label “Create your first project” and the example destination fixed. Leave the campaign unscheduled for review. The example destination does not lead to a working app. Supply your own project-creation URL when adapting this to your business. Open the saved email and review its layout and content. If the advice paragraph is not marked, select it and choose **Adaptyle**. The [personalization guide](/guides/personalize) shows the editor controls. ## Give Adaptyle its instruction and data Use this instruction on the marked paragraph: > Suggest a first project using this customer's notes. Give one concrete next step. Avoid work their teammates already own. If the notes do not contain a goal, ask them to name a project for work they already plan to do. Do not invent a role, deadline or previous activity. In **Data**, select Maya and check that her name and notes appear in the sample. Include any additional fields the instruction needs. If you use evaluated attributes or related records, select those expansions explicitly and inspect their returned values. Keep the greeting's name as a supplied value, and keep the button, destination, offer terms and layout outside the Adaptyle passage. The generated paragraph should read as part of the email. An instruction or Adaptyle label is editor information, not recipient content. ## Compare the finished messages Under **Personalize → Runs with**, choose **Model** and **Thinking**. Open **Preview**, select **Personalized**, then choose **Generate preview** and inspect the result. Select Leila, generate again, and repeat for Hugo. | Preview | Check the advice against the data | | ------- | ----------------------------------------------------------------------------------------------------------- | | Maya | It addresses launch sign-off and does not ask her to recreate Daniel's asset project. | | Leila | It uses the completed interviews to suggest synthesis or prioritization, not recruiting participants again. | | Hugo | It addresses the handoff and outstanding decision without inventing a team or project history. | Also preview a test contact with empty notes. The email should follow your fallback instruction without inventing personal context. If it does not, refine the instruction and compare the samples again. Each generation uses AI credits. ## Check the whole email Read the generated paragraph in the complete message. Check the name, subject, layout, button and destination as well as the advice. Resolve issues in **Checks**, and review **Estimate** before generating for a larger audience. For an inbox test, use **Send test** with an address you control after the sender is verified. This checks the rendered email in an inbox; it does not verify a production audience's marketing consent. You now have one draft and several inspected recipient messages. To use this email with real customers, replace the example destination, select a permissioned audience with real email addresses and follow [Send your first email](/quickstart/send-your-first-email). To send a reminder only while a signup still needs it, continue with [the onboarding journey](/guides/workflows). --- # Create a signup form Source: https://docs.maxclicks.ai/guides/forms # Create a signup form Build a newsletter signup with required email, optional first name and a clear consent choice. By the end, you will have a published form and a test contact whose saved fields and subscription you have checked. ### Start a new form In the top navigation, open **Forms** and click **Create form**. maxclicks creates it right away as "Untitled Form 1" and drops you in the builder, where you rename it in the header to something only you see, like "Homepage newsletter". The first section, **Display Mode**, is where you choose **inline** (sits inside the page) or **popup** (slides in over the page). ### Pick who signs up Choose which [contact type](/guides/contacts) the form fills, so the people who fill it out land in the right list. ### Choose what it asks for Add the fields you want to collect, like first name or company. An email field is always included and always required, since that is how you reach people. Mark any other field as required if you must have it. ### Set consent and confirmation Decide how people agree to hear from you: an explicitly described subscription on submission, only when they tick a checkbox, or none at all. Turn on **Enable Double Opt-In** to hold opted-in submissions for address confirmation. The confirmation link is single-use and expires after 24 hours. The hosted form uses its fixed check-email screen when double opt-in is enabled; your custom thank-you message or redirect applies when it is off. If someone declines an optional consent checkbox, no confirmation email is sent and their details can be stored immediately. ### Style it and publish Adjust the look so it matches your [brand](/guides/brand), and add a banner image if you like. Then click **Publish**, since a form only collects signups once it is published. Publishing also locks the form: the Builder tab is replaced by Overview and Share, and Share is where the embed code lives. To change anything later, click **Edit**, which unpublishes the form and takes it off its public URL until you publish it again. ### Add it to your site Open the **Share** tab, copy the embed snippet, and paste it into your website where you want the form to appear. Submit through the live form to verify it. Opted-in submissions wait for confirmation when double opt-in is enabled. Every form has built-in protections against abuse. Built-in rate limiting always applies, and you can turn on a CAPTCHA provider in the **Form Protection** section for stronger cover. If you pick a provider, add its keys under **Settings**, then **Forms Protection**. Until you do, that form cannot accept submissions at all. ## Test the complete signup Submit through the actual hosted link or embed after publication. Check that required fields reject missing values, the right contact schema receives the record, and the expected tags appear. Test both a new address and a matching existing contact. With an optional consent checkbox, test checked and unchecked submissions. Checked adds email consent and the form's selected topics; unchecked leaves existing consent alone and creates a new contact without an email subscription. A form submission is not a way to unsubscribe an existing customer. If double opt-in is enabled, confirm the email and verify the contact is saved only then. If CAPTCHA is configured, test it on the real site domain. A custom form must supply the provider's valid `captchaToken`; the embed handles this for you. Finally inspect any workflow or outgoing webhook triggered by the contact write. Auto-fill can use credits and downstream sends still need a verified sending domain. The [forms reference](/forms) explains the limits and the difference between submission and confirmation. ## After people sign up A completed submission creates or updates a contact in the selected schema. Forms match email-only contacts; they do not automatically merge product-identified contacts at the same address. Inspect the record before grouping it into an [audience](/guides/segments). To send a welcome email, use **Record Created or Updated** with a condition on the form's tag and the required subscription. There is no separate trigger for every form submission. See [forms and workflows](/forms#start-a-workflow-from-a-submission). --- # Import a contact list Source: https://docs.maxclicks.ai/guides/import # Import a contact list Bring your existing customer list into the model you have defined in maxclicks. Match the CSV headers to your fields, choose consent defaults carefully, and inspect the import results before using the records in an audience. ## Run the import ### Open the importer In the top navigation, open **Records** and pick the contact type this list belongs to. On that list, click the import button beside **Add**: it carries the type's plural name, for example **Import Customers**. That takes you to the type's **Import** tab, under **Schemas** in **Settings**, and opens the CSV dialog straight away. If you come to that tab on your own, click **Start import** to open the same dialog. ### Fill in the sample CSV Click **Download sample CSV** and paste your data underneath its header row. That header row is exactly the set of columns this contact type accepts, which is the whole point of starting from it. The standard dialog has no column-matching screen. Headers are writable field keys, such as `email`, `firstName`, `lastName`, `phone` and your custom keys. Whitespace is trimmed, but names are case sensitive: `Email` is not `email`. Unknown and read-only fields are omitted. The import table shows row outcomes, not a per-column warning, so inspect the saved profiles afterward. Use semicolons for multi-value cells, such as `vip;newsletter`. ### Upload it Add any tags you want applied to everyone in this file, then click **Upload CSV and start import** and choose your file. After upload, maxclicks creates an import job. The dialog shows upload progress separately from starting the job; leaving the page after the job is created does not stop it. A file can be up to 50 MB and 500,000 rows. One row over the row limit fails the whole file rather than the extra rows, so split a larger list before you upload it. ## Prepare values for the standard importer | Cell | How it is read | | ------------------------------------------- | -------------------------------------------------------------------------------------------- | | Blank | Omitted; an existing value is left unchanged rather than cleared | | Tags, subscriptions or a multi-select field | Semicolon-separated values, trimmed individually | | Boolean | `true`, `yes`, `1` and `on` mean true, case insensitively; other nonempty text becomes false | | Number or decimal | Numeric text is converted before field validation; check representative amounts after import | | Text and date fields | Trimmed text, then the field's normalizer validates it | Use explicit `true` and `false` for booleans. A misspelling such as `treu` becomes false rather than failing the row. Do not assume every conversion problem will appear in the failure log. On newly created records, missing writable fields use the schema's [creation policy](/attributes#decide-what-happens-when-a-value-is-missing): empty, default, required or AI autofill. Matching an existing record does not rerun those policies. You cannot import evaluated relationships or scheduled evaluated attributes; their values come from their own definitions. For a first run, include a new contact, an existing contact you intend to update and a row with an optional blank. Inspect their saved values before loading the remainder. ## Use a mapped import during onboarding When your CSV uses different names or needs a transformation, maxinja's onboarding flow can inspect the file and propose a mapping into a contact or object schema. Explain the intended identifiers, date formats, units and consent columns. The plan shows a readable mapping and a dry run over sampled rows, including example mapped inputs and any sample failures. Nothing is imported at that stage. Review and refine the mapping, then approve starting that prepared import. This is a separate path from the standard dialog's exact-key format. Sampling is bounded; a successful dry run does not validate every row in a large file. The background importer freezes its source, schema and mapping for the run. Changing a field definition or replacing the source file is not a way to repair the in-progress job; correct the input and prepare another import when needed. ## Only subscribe the people who agreed **Subscribe to email by default** is off, and it is the file's fallback rather than an override. Tick it only for people who have actually agreed to hear from you. Two limits decide what it actually does: * A row's own `subscriptions` cell wins. The checkbox applies only where that cell is blank, so a mixed list can be ticked and still land correctly row by row. * It reaches only the contacts this import **creates**. Someone already in maxclicks keeps the consent they had, unless their row carries a `subscriptions` cell. For people you have not asked yet, collect consent properly with a [signup form](/guides/forms) rather than assuming it here. ## Watch the run, then fix what failed Every import stays listed on the same **Import** tab, with its status and how many rows have been processed: **Pending**, **Running**, **Succeeded**, **Partial**, **Failed** or **Canceled**. A run that is still pending or running can be stopped from its row, which leaves it **Canceled** with the rows it already imported in place. The run first reads and seals its input, then processes checkpointed batches. An over-limit file or a source-level error can fail the run; an invalid row can fail independently while valid rows proceed. | Outcome | How to interpret it | | ------------- | ----------------------------------------------------------------------------------------------------------- | | **Succeeded** | No processed rows failed; still verify important field mappings on saved records | | **Partial** | Some records were written and some rows failed | | **Failed** | Read the run's reason and counters: it may be a source/preparation problem or a run with no successful rows | | **Canceled** | Further processing was stopped; previously committed records remain | Open the failed-row count to inspect sample row numbers, identifiers and reasons, or use **Download failure log** from the row menu. The UI retains up to 20 failure samples and the downloaded log contains up to 10,000 detailed failures, with an omitted-count indication for additional rows. Correct the affected rows in a fresh file rather than assuming the log always contains the whole original file. Deleting an import entry removes its history, not the contacts it already wrote. A running import must be canceled first, and deletion can still wait for an active worker or require reconciliation of unresolved AI enrichment. Read that error instead of starting repeated paid copies of the job. ## What happens to people you already have When an import matches an existing contact within the selected schema, it updates that record. Matching depends on the identifiers you supply; review conflicts and failed rows before using the list. Supply the maxclicks `id` to identify a particular existing record, or use a consistent source `userId`. Without a user ID, email is used; without either, phone is used. This is a priority rule within the selected schema, not a search across all identities until one happens to match. A supplied platform ID that is not found is a failed row. Identifier conflicts are also failed rows rather than automatic profile merges. A repeat import can update existing values. Give objects an `externalId` for the same reason. Default tags are added to the row's supplied tags, and explicit record values can replace existing state. Check identities, tags and preferences on a few matched records before treating an import as safe to repeat. ## Verify the data contract before scaling Choose three illustrative rows: a new customer, an existing customer with an updated plan, and a customer whose optional field is blank. Inspect the saved identity, plan, consent and custom fields. The blank should leave an existing value unchanged; the new record uses its creation policy. Compare those results with your intended mapping before importing the full list. If a field needs a different type or meaning, prepare a [setup change](/setup-changes) rather than trying to repair the schema through repeated imports. Review and complete the definition change, then use a newly prepared file or mapped plan for the corrected input. ## Next Now that your contacts are in, [build an audience](/guides/segments) to group the right people, then [send a broadcast](/guides/broadcast). --- # Meet maxinja Source: https://docs.maxclicks.ai/guides/maxinja # Meet maxinja Turn a repeat-purchase idea into a campaign you can review. In this guide, Halden Coffee wants customers to try a new roast. You will brief maxinja, inspect the saved audience and email, then refine the draft. ## Before you start You need workspace write access and available [AI credits](/billing/credits). maxinja access has no per-seat charge, but all AI work uses the shared prepaid workspace balance. No AI allowance is included. For this example, have customer records, related orders, the product facts and a published [brand kit](/brandkits). Use your own equivalent data and offer when following along. The example ends with an unscheduled draft. When you are ready to send, follow the domain, sender and consent checks in [send a broadcast](/guides/broadcast). ## Give it a little context Open the maxinja pane. Use `@` to attach your customer schema and brand kit. When refining an existing campaign, attach that campaign or select **Current view** above the message box. References identify the item you mean. The brief supplies the business decision: who to reach, why the message matters and what must stay fixed. ## How maxinja works ### Brief the campaign For a coffee campaign like Halden’s, start with: > Create a draft broadcast for customers whose latest order was at least 14 days ago. Introduce Colombia Huila coffee at $24 using the supplied product details and product link. Use purchase history to personalize the introduction with Adaptyle. Keep the price and product link fixed. Use the attached brand kit and leave it unscheduled. Replace the example’s product, price and audience with your own. Supply the product link and any claims the email is allowed to make. If the source data is missing, connect it before relying on that condition or personalization. ### Open the saved campaign Follow maxinja’s progress and answer any questions in the conversation. When it reports back, open the campaign. You should be able to inspect the audience, sender, email and schedule there. Check its report for unfinished requirements before treating the task as complete. ### Refine the same draft Attach the saved campaign and give a specific correction: > Keep this campaign’s audience and offer. Shorten the introduction to two sentences and relate the new roast to the customer’s previous coffee purchases. For someone with no useful purchase details, introduce the roast without assuming their preferences. Leave it unscheduled. Preview the changed email for several customers. A follow-up normally refines the same item; ask for a separate campaign when that is what you want. ## Review the artifact, then choose the next action Inspect the saved campaign as you would any campaign prepared by your team: | Review | What you are checking | | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Audience** | A customer whose latest order is older than 14 days matches. A recent buyer is excluded. Decide explicitly how a customer with no orders should be treated. | | **Email** | Product facts, price, sender and links match the brief. Adaptyle changes the intended passage without changing the offer. | | **Recipient previews** | An espresso buyer and a customer with different purchases receive relevant introductions. Missing purchase details do not become invented preferences. | | **Schedule and eligibility** | The campaign is unscheduled. Audience matches and people eligible to receive marketing email are separate counts. | A useful follow-up is: “Explain why this customer is included,” with that customer and campaign attached. Investigate an unexpected result before changing the condition. maxinja saves edits as it works. If you ask it to send or publish, it can take that action without another assistant approval stage. Keep the requested draft state explicit until your review is complete. ## Two places you will find it Use the **chat pane** for complete campaigns, reusable email templates and workflows. It can also investigate customer data and explain the current setup. Use the **Ask maxinja** control within a feature for its specific logic: a reusable segment’s condition, an evaluated attribute or a workflow step. To reuse this campaign’s audience later, create a [segment](/guides/segments) with the same intended condition in its own editor. ## Good to know * **Authoring and personalization are separate.** maxinja saves the draft and its instructions. [Adaptyle](/adaptyle) generates the marked content when a recipient’s email is rendered. * **Stopping keeps completed edits.** Reopen the saved item after an interruption and continue from that state. Retrying a chat message does not undo product changes. See [while maxinja works](/maxinja/agentic). * **AI work uses credits.** Briefs, revisions and personalized previews can all add usage. An admin can review spend by period, category and member under **Settings → Billing → Usage**. There is no domain-verification credit gift or monthly AI allowance; eligible initial setup assistance has separate platform funding. See [billing](/billing). ## Next [Personalize the email](/guides/personalize) and compare recipient previews, then use [send a broadcast](/guides/broadcast) to prepare the campaign for delivery. --- # What you can ask maxinja to do Source: https://docs.maxclicks.ai/guides/maxinja-tasks # What you can ask maxinja to do Use these briefs to build a campaign, investigate an audience or refine a journey. Each names the data you need and the result to review. Replace the examples with your own customer behavior, offer and links. New to the assistant? Follow [your first campaign brief](/guides/maxinja) before using this library. ## Ask, review, refine A useful brief includes **who**, **what they should do**, **what must stay fixed** and **how far maxinja should go**. Start with a draft, inspect the saved work and give a specific follow-up. ## Prompts to copy ### Bring previous buyers back **Have ready:** customers, related orders, the product facts and link, and a brand kit. > Create a draft broadcast for customers with at least two completed orders and no completed order in the last 60 days. Introduce the attached product using their purchase history. Use Adaptyle for the introduction; keep the supplied price, offer and product link fixed. Use the attached brand kit and leave it unscheduled. **Review:** a repeat buyer should match; a recent buyer and a first-time buyer should not. Compare the email for customers with different purchases. Check that limited history produces factual copy, not an assumed preference. ### Help new signups reach a milestone **Have ready:** a signup event, the customer it belongs to, and project records that identify their owner. > Build an unpublished journey that starts when a customer signs up. Wait two days, then query their own projects. Stop if they have created a project. Otherwise prepare an email asking them to create their first project, using the supplied product instructions and link. Show me the trigger, the query result and both branches. Keep it unpublished. **Review:** the query runs after the wait. A teammate’s project should not count as the customer’s own. Check the customer with a project, the customer without one and the handling of missing data. Inspect the email’s sender and samples before publishing. Add an [AI step](/workflows/ai) when the journey needs to interpret context into a result that later steps can use. Add [Adaptyle](/adaptyle) when the email itself needs recipient-specific wording. The [workflow guide](/guides/workflows) explains the difference in a complete journey. ### Improve an existing email **Have ready:** the campaign or template, its intended action and the passage to change. > Shorten the introduction in this email to two sentences. Explain why a customer who buys espresso might try the supplied new roast. Keep the subject, product facts, price, button text, links and sender unchanged. Leave the campaign unscheduled. **Review:** compare the saved passage with the previous version. Check that the requested scope was respected and preview several recipients if the email uses Adaptyle. To change just one block or the subject, use the editor’s maxinja input with that part selected. ### Investigate before planning a campaign **Have ready:** customer and order records with the dates and statuses needed to answer the question. > How many customers have at least two completed orders but none in the last 60 days? Show the condition you used and representative matches. Explain how customers with no order date were treated. Do not create a campaign yet. **Review:** compare the explanation with known customer records. These workspace data queries are read-only. They help establish an audience opportunity; they do not measure the revenue a future campaign will produce. ## Point it at something you already have Type `@` to attach a campaign, template, workflow, schema, segment or brand kit. For an individual customer, open the record and choose **Current view**; the picker does not search all customers. References identify the exact item to investigate or change. For follow-ups, keep that saved item attached: “On this draft, exclude customers who purchased in the last seven days.” If you want a second campaign, explicitly ask for a separate one. ## Say who and what Replace vague requests with a customer decision and a reviewable result: | Too little direction | A more useful brief | | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | “Make this better.” | “Shorten the introduction to two sentences. Keep the price and CTA fixed.” | | “Email inactive users.” | “Draft a campaign for signups with no project of their own after two days. Exclude those who have started.” | | “Personalize the message.” | “Relate the introduction to recorded purchase history. If it is missing, describe the supplied product without assuming a preference.” | The data and product facts must exist in the workspace or be supplied with the brief. maxinja cannot establish a customer’s behavior from a marketing goal alone. ## Give the request an execution boundary Name the result you want: an unscheduled campaign, an unpublished workflow or a specific send or publication. To review an existing campaign: > Review this campaign’s audience, offer, sender, customer data and missing-value behavior. Fix the draft and show what is still blocking it. Leave it unscheduled. maxinja saves edits directly. It can carry out a requested send or publication without another assistant approval stage, subject to product validation and permissions. A completed draft edit does not establish delivery or a completed workflow run. You can steer active work with a correction. **Stop** interrupts further work but keeps completed edits and effects. Editing or deleting chat messages does not restore earlier product state. Workspace conversations require write access. See [while maxinja works](/maxinja/agentic). ## What it costs AI requests use [credits](/billing). The model, supplied context and work performed affect usage, including preview generation and revisions. An assistant brief has no fixed total price. Recipient personalization and workflow AI add their own execution costs. An admin can inspect the balance under **Settings → Billing → Overview** and usage breakdowns under **Usage**. There is no domain-verification credit gift or monthly AI allowance. Eligible initial setup assistance has separate platform funding; ordinary authoring and previews use the workspace wallet. ## maxinja outside the chat Use the chat for complete templates, broadcasts and workflows. Other features have their own controls: | To create | Start here | | -------------------------------- | ------------------------------------------------------------------------------------------------------- | | A reusable audience | Describe its condition in [Segments](/guides/segments). | | A customer lookup or calculation | Add an [Evaluated on demand attribute](/evaluated-attributes). | | A webhook condition | Use the generation control in [Webhooks](/webhooks). | | A reusable activity measure | Define a source and calculation with [Scheduled evaluated attributes](/attributes#computed-attributes). | Initial onboarding separately assists with the data model and CSV mapping while setup is incomplete. ## Next Open the saved campaign and follow [send a broadcast](/guides/broadcast), or compare recipient content in [personalize an email](/guides/personalize). --- # Personalize with AI Source: https://docs.maxclicks.ai/guides/personalize # Personalize with AI Create one email whose message changes with the customer’s circumstances. In this guide, Loopwell uses [Adaptyle](/adaptyle) to suggest a first project to three new signups. The design and call to action stay the same. ## What you will build Maya is coordinating a launch, Leila has interview notes to organize, and Hugo is preparing a client handoff. Each has zero projects of their own. They need different advice from the same onboarding email. These saved, unedited outputs were generated with fictional customer data. Maya gets a launch sign-off project, Leila gets an interview-synthesis project, and Hugo gets a client-handoff project. Choose **Template** to read the shared instruction. ## Before you begin You need a draft [template](/guides/templates), [broadcast](/guides/broadcast) or [workflow email](/guides/workflows), a sender, a published brand kit and AI credits for previews. Use your own customer goal and progress fields; Loopwell is an example, not a required data model. For this example, the email needs each customer’s first name, goal, own-project count, work already owned by teammates and any unfinished task. Stored fields arrive with the record. Select [Evaluated attributes](/evaluated-attributes) and related records in **Data** when you need them. ## Mark a passage for maxinja ### Choose the paragraph that should change Open the email in the visual composer. Keep the greeting, button and required campaign facts outside the paragraph you want to personalize. For Loopwell, only the project recommendation changes. ### Add the Adaptyle instruction Select that paragraph and choose **Adaptyle** from the text menu. In the block’s instruction, describe the writing job: > Suggest a useful first project from their goal, progress and team context. Give one next step. Avoid work their teammates have already started. Add writing rules that fit your campaign: use only supplied facts, keep the paragraph short and do not invent product features. Type `/` in the instruction for suggestions based on the data the email carries. ### Select the data behind the message Open **Data**, choose a sample customer and inspect the field values. For Maya, the relevant difference is that Daniel owns launch assets while sales and design sign-off is still missing. Her own-project count is zero; her teammate’s project does not make it one. Choose your published brand kit for writing guidance. If a required field is absent, supply it or give the instruction an explicit fallback, such as “If their goal is missing, describe how to name a project and add one next step without assuming a goal.” ### Generate the recipient preview Under **Personalize → Runs with**, choose **Model** and **Thinking**. Both are required; the workspace’s default model does not fill them in. Open **Preview**, choose **Personalized**, and click **Generate preview**. **Base** shows the email without the AI rewrite. **Personalized** shows the generated email for the selected data. The Adaptyle instruction and editing outline do not appear in the recipient’s email. ### Compare another customer Return to **Data**, select a different contact and regenerate. Compare Leila’s interview notes with Hugo’s handoff: the paragraph should reflect their different work. Check the greeting, unchanged call to action and its destination as well as the generated paragraph. ## Find the controls in the composer To stop personalizing a passage, select it and choose **Remove Adaptyle**. The authored content remains. To refine the saved draft with the assistant, ask maxinja to change that paragraph; to change future recipient wording, edit its Adaptyle instruction. ## Compare a representative set Check these cases before using the email in a campaign: | Customer context | What to check | | ---------------------------------------- | ------------------------------------------------------------------------- | | A clear goal and complete data | The recommendation follows that goal and cites only supplied facts. | | A different goal or progress state | The advice changes where the context calls for it. | | A teammate has already done related work | The message does not treat the teammate’s activity as the customer’s own. | | Missing goal or history | The fallback makes no claim about information you do not have. | Open **Checks** in the editor’s right panel. Resolve its issues, inspect representative personalized samples and send an inbox test. A clean draft and a successful sample do not establish that every future output will be correct or that a campaign has been delivered. For a broadcast that later stops on a quality hold, inspect the actual recipient output in **Quality review**. That review concerns a particular generated email; it is separate from approving the draft or looking at a sample. See [review a quality hold](/broadcast#review-a-quality-hold). If a render rejects a protected link, markup or fixed text, narrow the instruction. Make layout changes in the draft and keep recipient-time instructions focused on writing. See [Adaptyle validation](/adaptyle#protect-the-parts-that-must-stay-fixed). ## What it costs Previews, regenerations and each recipient render that needs Adaptyle use AI [credits](/guides/billing). Select only the passages where context changes the message. Liquid substitutions and fixed copy do not themselves need a generative call. Use **Personalize → Estimate** before scaling. Subject and body are generated together; a correction or campaign review can add calls, so one prompt does not equal one billable call. Sending allowances, overages and other operation charges are separate. The estimate marks unknown components, and one selected sample is not an audience-wide average. ## Getting good results * Give the instruction a concrete writing job, such as recommending a first project from the customer’s recorded goal. * Select the required customer context and state what to do when it is missing. * Keep links, offer terms and required wording outside AI regions. * Compare different customers. A later generation can produce different wording. * Keep the email focused. More than 10 instructions triggers a warning. ## Related Use the same email in a [broadcast](/guides/broadcast) or a [workflow](/guides/workflows). For code-editor syntax, rendering rules and failure recovery, see the [Adaptyle reference](/adaptyle). --- # Build a repeat-purchase campaign Source: https://docs.maxclicks.ai/guides/repeat-purchase # Build a repeat-purchase campaign Give returning customers a reason to order again. This walkthrough uses Halden, a fictional coffee business, to connect purchase history, a campaign audience and a personal product introduction. The goal is a new completed order. An email open or click is an earlier signal, not the purchase itself. ## Start with the customer decision Halden is introducing a single-origin coffee. It wants to reach past buyers who have not ordered recently. The product and destination stay the same, while Adaptyle explains the coffee using each customer's recorded preferences. ## Prepare the data You need customer records, linked orders and the product facts you intend to use. Your team or integration must keep that source data current. | Data | Why the campaign needs it | | -------------------------------------------------------- | --------------------------------------------------------------------- | | Customer identifier and email preferences | Match orders to the right person and determine marketing eligibility. | | Order identifier, customer identifier, status and date | Count completed orders and find the last purchase. | | Product name, description, current price and destination | Keep the campaign's claims and offer tied to supplied facts. | | Brewing method or stated preference, when available | Give the introduction relevant context without guessing. | Check the actual identifiers that connect orders to customers. A similar name does not establish a relationship. [Understand your data](/guides/data) explains setting up these fields; [import a list](/guides/import) or use the [Records API](/api/records) to populate them. ## Define who should hear from you Create **On demand** [evaluated attributes](/evaluated-attributes) for completed order count and the latest completed order when those facts come from current related Order records. Use **Scheduled** evaluation for a complete published calculation from Event history instead. Review the source, timing and data window before treating either as today's purchase history. Test a customer with several orders, one with none and one whose latest order is not completed. For this campaign, use a business rule such as **at least two completed orders and no completed order in the last 30 days**. Adjust that interval to your product's buying cycle. Open **Segments → New segment**, choose your contact type and describe that rule in the condition prompt. maxinja generates the condition there using your actual schema and fields. Save the reviewed segment. Open the matching records. Confirm that a recent buyer is excluded and a qualifying returning buyer is included. Marketing consent and suppressions are checked separately from matching the audience. ## Brief maxinja and inspect the draft > Create an unscheduled broadcast for this returning-customer segment. Introduce the supplied single-origin coffee. Keep the product name, price, offer terms and product link fixed. Use Adaptyle for the introduction, drawing on the customer's selected order history and brewing preference. If those details are missing, introduce the coffee without claiming a preference or purchase. Leave the draft for review. Attach or select the actual product details. Open the saved broadcast and verify its contact type, segment and matching recipients. In **Composer**, inspect the authored product section and link; the agent's summary is not a substitute for checking the saved campaign. ## Check two customers and a fallback In **Data**, select the purchase history and preference fields the instruction needs. Choose a published brand kit and set **Model** and **Thinking** under **Personalize → Runs with**. Open **Preview → Personalized → Generate preview** for each test case. | Customer context | What to inspect | | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | Pour-over preference and relevant purchase history | The introduction can use those facts, but should not invent a tasting opinion or claim the customer tried the new product. | | Espresso preference and different previous orders | The wording should reflect that context while preserving the supplied product facts and offer. | | Missing preference or order detail | The introduction should use the fallback without claiming an unsupported personal history. | Read each full email, including the subject and destination. Refine the instruction if the copy adds unsupported claims. See [personalization checks](/adaptyle#protect-the-parts-that-must-stay-fixed). ## Send and inspect the response Use [the sending checklist](/quickstart/send-your-first-email) to verify the domain, sender, recipients, consent, rendered samples and estimated usage before scheduling. Adaptyle rendering uses AI credits for each recipient who needs generation, in addition to applicable sending usage. Afterward, inspect recipient delivery outcomes and clicks in [reports](/guides/reports). Use completed orders from your business data to assess whether customers purchased again. Define the purchase window and customer matching before interpreting results; clicks alone do not establish revenue or prove the campaign caused an order. For a follow-up journey, check for a new completed order after the wait and before another email. The [workflow walkthrough](/guides/workflows) teaches that check-before-reminding pattern with an onboarding example; apply it to your order records and buying cycle. Put the newer-order **Query** after **Delay**, and compile the **If/Else** decision. Confirm that the order belongs to this buyer and follows the triggering purchase. A valid empty result can continue to the reminder; a failed lookup is not evidence that the buyer never purchased. If the live journey needs containment, **Pause** holds further execution. **Stop new runs** closes enrollment while already-enrolled buyers finish. **Terminate run** in **Runs** ends one journey and its children, retaining accepted effects. Publishing a corrected check resumes a pause, while old runs keep their old version; inspect them before release. --- # See your results Source: https://docs.maxclicks.ai/guides/reports # See your results Use campaign results to decide what to investigate next: delivery, audience fit, message or timing. maxclicks records email delivery and response signals at campaign and workspace level. This guide explains where to read them and what they can establish. ### Open the email you sent In the top navigation bar, open **Broadcasts** and click the one you want to check. A broadcast you have already scheduled or sent opens on its **Metrics** tab. ### Read the top-line numbers You will see delivery and response counts. Check each definition below before interpreting them as people reached. Results keep arriving for a while after you send, so click **Refresh** to pull the latest numbers. ### Understand what each number means Here is what the main numbers tell you: * **Sent in a campaign or template funnel**: the number of email records included in that report. Pending, rejected or failed records can be included; this total alone does not confirm provider acceptance. * **Sent on an individual email**: the provider-accepted state, before a later delivery or engagement state takes precedence. * **Delivered**: the receiving mail server accepted the email. That is not a promise of inbox placement, since it can still land in spam. * **Opened**: an email had at least one recorded open. The summary counts each email once, not every open and not distinct contacts across several sends. It does not prove that a person read the message. * **Clicked**: an email had at least one recorded link click. Repeated clicks on that email do not increase the summary count. It records an interaction, not a purchase or activation. * **Unsubscribed**: email records marked unsubscribed. This is not a total of every change in the contact’s preference history; a preference change without an associated email need not increase this metric. * **Bounced**: the email could not be delivered, often because the address is wrong or no longer exists. * **Complained**: the person marked it as spam. ### Act on what you see Start with delivery: inspect failed recipients and bounces before interpreting engagement. Compare response across similar audiences and sending periods. Low clicks can justify reviewing the offer, content or call to action; rising unsubscribes or complaints can justify reviewing audience fit and frequency. These signals suggest what to investigate, rather than establishing a cause. Ask maxinja: "How did my last broadcast do, and what should I change next time?" It reads the results and gives you plain suggestions. ## Read the rate and its denominator The same count can produce different rates. The campaign and template summary’s **Messages clicked** card shows **Click-to-open rate**; the workspace overview shows **Click rate**. Compare the labeled rate rather than assuming those cards use the same denominator. | Rate | Calculation | | ------------------------ | --------------------------------------------------------------- | | Delivery rate | Emails with a delivery event ÷ the report’s Sent count. | | Open rate | Emails with at least one open ÷ delivered emails. | | Click rate | Emails with at least one click ÷ delivered emails. | | Click-to-open rate | Emails with at least one click ÷ emails with at least one open. | | Bounce or complaint rate | Emails with that feedback ÷ the report’s Sent count. | | Unsubscribe rate | Emails marked unsubscribed ÷ delivered emails. | Each calculation is multiplied by 100. A zero denominator displays 0%; that does not establish that a populated audience had no engagement. Counts come from recorded events and can change when feedback arrives. For example, 100 email records, 80 with delivery, 40 with an open and eight with a click produce 80% delivery, 50% opens, 10% clicks and 20% click-to-open. These are illustrative counts. The click-to-open formula uses all emails with clicks; it does not require a separate recorded open on each clicked email. A client can record a click without loading the open-tracking image. Campaign and template funnels count their linked email records, including records that have not been accepted. Workspace metrics instead select emails with a recorded send timestamp in the chosen period. The two reports can therefore cover different populations while a campaign is preparing. Check individual **Emails** statuses before describing a total as messages sent. ## A note on bounces and complaints Hard bounces and complaints create address suppressions automatically. Any suppression blocks future marketing; transactional messages are blocked by bounce suppressions. Suppression applies to the address, including other contacts that share it. Review [consent and suppression behavior](/types-emails) before interpreting an excluded recipient as a campaign error. ## Trace the gap before planning another send | Gap you see | Inspect next | | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | Audience count exceeds sent emails | Missing or duplicate addresses, channel/topic consent, suppression, preparation errors and cancellations. | | Preparation has not progressed | Campaign credit or quality hold, current readiness and the scheduled time. | | Sent exceeds delivered | Individual email activity and provider delivery errors; allow for events still arriving. | | Delivery is healthy but clicks are weak | Representative messages, audience fit and destination experience. Metrics suggest an investigation, not a proven cause. | | A workflow has waiting runs | Delay deadlines, workflow pause, available credits, capacity and event-history evidence. | Do not resend the whole audience to fill a reporting gap. A delayed event or uncertain provider acceptance can hide a message that was already sent. Use recipient history to decide what follow-up is appropriate. ## See everything at once A broadcast tells you how one email did. **Metrics**, in the top navigation bar, rolls up every email your space sent over a date range: sent, delivered, opened, clicked, bounced, complained, and unsubscribed. Delivered, opened, clicked, and unsubscribed are each shown against the previous period, which is where a trend becomes visible. The page also ranks your best broadcasts, templates, and workflows. You can filter by sending domain and by marketing or transactional. The presets run from today to the last 30 days, and a custom range can span up to 90 days: past that, the request is refused rather than trimmed. Presets use the browser’s local day boundaries. Use the same displayed date range and time zone when comparing with an external report. The range selects emails by their send timestamp. Opens, clicks and delivery feedback recorded later still contribute to those selected emails, so reopening an earlier period can show different engagement. It is a report about emails sent in that period, rather than every interaction that occurred during it. The previous period is the immediately preceding range of equal length, with the same sending filters. Comparison percentages are relative changes: an open rate rising from 20% to 30% displays a 50% change, while the absolute difference is 10 percentage points. When the previous value is zero, the displayed comparison is 100% for any positive current value and 0% when both are zero; read the underlying counts. The ranked lists show up to 10 broadcasts, templates or workflows, not a complete inventory. ## Connect the signal to the business outcome A click is not an order, and an open is not product activation. Bring meaningful business [events](/events), such as a completed purchase or first project, into your customer model when you need those facts for a segment or journey. The email metrics alone do not establish revenue attribution. ## Next Head back to [Send a broadcast](/guides/broadcast), or set up a [workflow](/guides/workflows) that sends the right email on its own. --- # Create a segment Source: https://docs.maxclicks.ai/guides/segments # Create a segment Turn a campaign hypothesis into an audience you can inspect. Describe the customer behavior, time window and exclusions that matter; maxinja generates the segment condition and shows the matching contacts. A saved segment evaluates the current data when used. There is no rule builder behind this. Describing the group is the only way to define one, so every segment costs a maxinja request in [credits](/guides/billing). Generating or refining the condition uses your space’s available credits. Eligible new Free spaces can receive funded setup assistance from maxinja; that does not make every AI feature free before [domain verification](/guides/domain). ### Start a new segment In the top navigation bar, open **Segments** and click **Create segment**. Give it a clear name you will recognize later, like "Returning regulars". ### Describe who belongs in it Type who you want to include, in plain English, for example "customers with at least two recorded orders whose latest order was at least 14 days ago". Select the contact schema first if the workspace has several. Specify the actual order/customer join, the status that counts, and the timestamp used for “latest.” Generate the condition; maxinja uses the available data model to propose the rule. ### Check the group maxinja built maxinja shows a short summary of who it included, plus a count and a list of the matching contacts. Read the summary and inspect the people to check what it matches. In the Loopwell example below, Maya and Amara have no projects of their own; a teammate’s project does not count. ### Refine it in your own words Add another sentence to narrow it, like "only include people in the United States". maxinja updates the group and the count refreshes. Generate each refinement and recheck the results before saving. An instruction still sitting in the prompt is not itself a saved rule. Before saving, test a known exclusion as well as an inclusion. For a scheduled activity field, decide whether an empty value means “no observed activity” or “not yet calculated,” and check its published freshness. Refreshing this preview does not recalculate the source history. ## Check who the related records belong to Use this review against your own records; these are illustrative checks, not extra results from the screenshots. For **customers with two completed orders and none in the last 14 days**, include a shopper whose own two completed orders are both older than 14 days. Exclude a shopper with only one completed order and one canceled order. For **users with no projects of their own**, include a user with no project linked to their creator ID. Exclude their teammate who created a project in the same account. Open the matching contact and inspect the source identifiers, dates and statuses. A plausible total can still hide a wrong relationship. For account or order setup, use [Objects](/objects#how-objects-connect-to-people). ## Reuse your segment Once saved, a segment can be selected by the supported audience and workflow controls. Pick it when you [send a broadcast](/guides/broadcast) so the email only goes to that group, or use it inside a [workflow](/guides/workflows). You can also build a new segment on top of an existing one, for example "people in my Active customers segment who also live in Europe". ## Understand changes after saving Membership is evaluated when used and can change as records change. A broadcast later seals the audience for that send; it does not continually enroll later matches. A segment count also precedes consent, suppression and destination checks. Search narrows the preview, not the saved rule. Counts refresh when a new first page loads and are reused during pagination. If a generated definition depends on this segment, editing or deleting the source segment can be blocked; create a separate audience when a changed meaning should not affect those dependents. ## Next With your segment ready, [send a broadcast](/guides/broadcast) to it. --- # Set up your space Source: https://docs.maxclicks.ai/guides/setup # Set up your space Set up the workspace your team and agents will work from: business identity, customer model, data region and sending domain. This guide covers manual setup through **Workspace**, **Brand** and **Contacts**, plus what to review when starting from your website. Domain verification remains a separate step before sending. ## Choose how to start Choose **Set it up myself**. The three stages are **Workspace**, **Brand** and **Contacts**. Enter the real business identity, postal address, sending domain and customer type before creating the workspace. You configure design assets and voice guidance in a brand kit after setup. ## Step 1: Workspace You become the first admin of the space you create, which is the role that invites teammates. ### Give your workspace a name Type the name your team will recognize, usually your company name. maxclicks turns it into a **handle**, the short web address for your workspace, and you can edit it. The handle has to be unique across maxclicks, so a short version of your company name works best. ### Pick your data region Choose **Data region** from the regions offered for the workspace. Check the required region with your team before creating it; the selection configures the workspace's regional resources. This one is permanent. There is no way to move a workspace to another region after it is created. ### Add a workspace logo (optional) Upload a logo or image to make the space recognizable in the workspace switcher. You can change this any time in settings. ## Step 2: Brand These details say who your emails come from: your brand name, your logo, the domain you send from, and the postal address that has to appear in your footer. ### Enter your brand name and website Fill in your **brand name**, the business identity your content uses, and your **website**. You can also upload a **brand logo**, which appears at the top of the emails you send, a square **brand icon**, and a short **summary** of what you sell and who you sell it to, which maxinja reads when it writes in your voice. ### Choose your sending domain **Sending domain** is where your emails go out from, such as mail.acme.com. maxclicks suggests a `mail.` subdomain of your website, which gives marketing email a distinct sending identity. A subdomain does not guarantee reputation isolation; see [subdomain guidance](/subdomain). Naming it here does not verify it: you add the DNS records after setup, in the section below. ### Add your business address Use a real business postal address for the email footer, where your business can receive mail. The country you enter also sets the currency your workspace is billed in. Colors, fonts, assets, and social links live in a brand kit, which you create separately under **Brand Kits**. A new kit starts from a default color palette and style pack you can adjust. See [Set up your brand](/guides/brand). ## Step 3: Contacts The last step creates the first contact type in your space: the kind of person you email, named in the singular and the plural. ### Start from a preset or name your own Pick one of the presets (Customers, Leads, Subscribers, Students, Members, Donors, Clients), or type your own **Contact name**, which maxclicks pluralizes and turns into a slug. The name shows up throughout the app, in records, imports, segments, and broadcasts. ### Create the space Submit, and maxclicks builds the workspace, the brand and the contact type together. You land in the app with an empty contact list. You can add more contact types later under **Settings**, then **Schemas**. See [Understand your data](/guides/data). ## Connect a sending domain Now you prove you own the domain you picked, by adding a few records at your DNS provider. Those records sign your mail, so inbox providers can check it really came from you. Do this before anything else you plan to send. Campaign, template, workflow and editor test emails require your verified sender domain. Form double-opt-in confirmation uses its own platform address; it does not unlock ordinary sending. First domain verification enables sending and ends eligibility for platform-funded initial onboarding; it grants no AI credits. Eligible new spaces can receive funded maxinja setup assistance before that point; ordinary AI previews and other generation still need available workspace credits. ### Open your domain Setup already asked which domain you send from, so it is waiting for you. Open **Settings**, go to **Domains**, and select it. maxclicks shows you a short list of DNS records to add. To send from a different domain instead, use **Add domain** here. ### Add the records at your domain provider Sign in wherever you manage your domain (GoDaddy, Namecheap, or Cloudflare, for example) and paste in the records maxclicks gave you. It is a copy and paste job, no coding. Ask whoever looks after your website if you are unsure. ### Start verification and wait Come back to maxclicks and click **Start verification check**. Follow the individual record statuses and the overall **Verified For Sending** state. A value saved in your DNS dashboard can take time to become visible to the verification process. Your DNS provider and cached DNS responses can add a delay. Full detail on the records and the status table is in [Connect your domain](/guides/domain). ### You are ready to send Your first domain automatically becomes your space default, so new emails use it without any extra step. Once it shows as verified, it is ready to send from. You only need one verified domain to start. If you ever remove the records at your provider, maxclicks notices and marks the domain unverified again. ## Related Next, [take a tour](/guides/tour) of your space, or jump straight to [adding your contacts](/guides/contacts). --- # Invite your team Source: https://docs.maxclicks.ai/guides/team # Invite your team Give campaign owners, operators and administrators the access their work requires. Workspace roles determine who can edit campaigns, manage members and control billing. This guide shows how to invite teammates and choose a role. ### Open your members list Open the space menu in the top bar and choose **Members**. You will see everyone who already has access. ### Invite someone Click **Invite team members**, then type their email addresses. You can paste several at once, separated by commas, spaces, or new lines. ### Choose what they can do Pick a role in **Invite as** before you send: * **Member** does the everyday work: add contacts, build audiences, design emails, send broadcasts, and set up workflows. * **Non-paid Admin** runs the space, including members and billing, but cannot edit content or use maxinja chat, including questions intended to be read-only. * **Admin** has full access: everything a member can do, plus members, billing, and workspace settings. Give admin only to the people who need to run the space itself. These roles control permissions. Collaborators and maxinja access have no per-seat charge; paid AI work uses the shared workspace wallet. ### Send the invite Send it. Anyone who already has a maxclicks account joins straight away, and everyone else gets an invitation email with a link that works for 14 days. You can change anyone's access or remove them from this same page later. ## A few things to know * You need admin access yourself to invite or remove people. A Non-paid Admin can only hand out the Non-paid Admin and Member roles. * Invite people at their work email address. maxclicks does not accept accounts on personal providers like gmail.com or outlook.com, so an invite to one is refused when you send it. * Removing someone takes away their access right away, but the work they already did stays in your space. ## Finish the access review Check the selected workspace and the new member's role after adding them. Existing account holders join immediately; a new user joins through the invitation. maxinja requires write permission and available prepaid credit, with no per-seat charge or monthly AI allowance. You cannot change your own role or remove yourself here, and the workspace must retain an admin. A Non-paid Admin cannot promote, change or remove a full Admin. See [members](/members) for the role and API-key rules. ## Related Now that your team is in, help them find their way around in [Take a tour](/guides/tour). --- # Reuse a template Source: https://docs.maxclicks.ai/guides/templates # Reuse a template Create an order confirmation your product can send after checkout. You will finish with a saved transactional email, a customer recipient, an order input and a clear handoff for the developer who connects the checkout event. Have a customer contact schema, an order object schema and a representative record in each. If your application supplies order details as JSON instead, agree its fields with your developer first. You also need a verified sender for an inbox test. The Halden media below shows actual saved templates and editor controls; use your own order fields and product facts. ## Build the template ### Create it In the top navigation, open **Templates** and click **New template**. You land on "Choose your email's starting place", where you pick the editor you want and the sending mode. It is created as an untitled template, and you rename it from its header afterwards to something clear like "Order confirmation". ### Design the email Build it with text, images and buttons in the editor. It picks up your [brand](/guides/brand) automatically. For the details that change on every send, drop in a [merge tag](/liquid). Open **Data** first. When your workspace has a default contact schema, the editor starts with that contact property and selects it as **To**. Confirm it is the customer you intend, then use **Add property** for the order. Each merge tag uses the exact key shown in this panel. If the rows are `customer` and `order`, you can use `{{ customer.firstName | default: "there" }}` and your order’s actual fields. For this confirmation, keep the order number, items and total supplied by the application or stored order. An example message is “Thanks for your order” followed by those facts and a button to its status page. The marketing preview below illustrates the shared editor, not this confirmation’s sending mode. For the parts you want written fresh per person, add an [AI instruction](/guides/personalize). ### Set the sender and the topic Choose which verified address the email comes from. What you do next depends on the sending mode you picked when you created it, which cannot be changed afterwards. **Transactional** fits this order confirmation. It does not require marketing subscriptions and carries no marketing unsubscribe link. The sender, address, bounce suppressions and available sending capacity still have to pass their checks. **Marketing** reaches only the contacts who consented to email. Pick its **Topic** in the email-level **Delivery** settings when you group email that way, and maxclicks adds the unsubscribe link for you. An address that keeps bouncing is skipped in either mode. ### Preview it Click **Preview** to inspect a rendered sample. Personalized wording can differ on a later render. The app's readiness check requires valid sample data and a successful preview. Review several examples, because a single sample does not prove every future request will have complete data. Your edits take effect on the very next send, so changing the wording later never means starting over. Ask maxinja: "Draft a shipping confirmation template with a tracking button." It writes the email and leaves the right spots for each person's details. ## Before it can send Open **Checks** in the editor’s right panel to see what the draft still needs. Actual application sends also validate their supplied data and current sending conditions: | What it says | What to do | | ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | No contact or email data property is selected to send to | In the editor's **Data** panel, set **To** to the contact or email address this email goes to | | No sender is specified | Choose the address it comes from | | No email content is specified | Add content in the editor | | You need to preview your email first | Select valid sample data and render a successful preview; generate the personalized preview if needed | | Subject line is empty | Write a subject | | Body is empty | Write a body | | Checking this content for AI prompts. Try again in a moment | Nothing to do. maxclicks is scanning the content, and the check clears on its own | | Missing personalization model settings | Open **Personalize** in the right rail and configure the model and thinking depth for the email's [AI instructions](/guides/personalize) | The toolbar's **Send test** mails a sample to an address you type. That is for checking your work, not for reaching real people. It is still a real send: you need a sender picked in **From** on a verified [domain](/guides/domain) for the send to succeed, the message goes out as transactional from that sender, and it counts against your monthly email allowance, so check your remaining [email allowance](/billing). ## Hand it to your app A saved template has no send button of its own: your app asks for each send. The template's **Send** tab collects what you need for that: the template ID, the data this template expects and which property is the recipient, plus an example request you can copy. You only have to send enough to identify each record. Everything else your email uses is read from the stored record at send time. Extra fields you do send are saved to the record first, which is how you fill in a contact who does not exist yet. For the request itself, see the [Templates API](/api/templates). ## Check the first connected send Have the application trigger the confirmation for a deliberate test order after your preview and inbox review. Inspect the response and retain its email ID, then find that attempt in the template’s **Emails** tab. Confirm the customer, order facts, links and activity match that one checkout. **Sent** is provider acceptance; wait for delivery feedback before treating it as delivered. If the inputs do not match, compare the application payload with the **Send** tab. If the result is uncertain, investigate the existing email before requesting another one. ## Agree the handoff with your developer Share the template ID, the exact Data keys and shapes, the recipient path and the business event that should trigger it. Supplying record fields can change stored customer data before rendering; use identifiers unless those updates are intended. Agree one idempotency key for each logical message and preserve it on retries. The application should inspect returned send status and keep the email ID for delivery investigation. A successful HTTP response is not proof the recipient received it. Coordinate saved content changes with the application owner. New requests use the saved template without a publication stage. If the data contract or purpose changes, a separate template and deliberate ID switch can make the rollout easier to review. ## When to use a template Reach for a template when the same email goes to one person at a time with their own information, and your app decides when. For a one-time email to a whole group, use a [broadcast](/guides/broadcast). For an email maxclicks sends on its own, build a [workflow](/guides/workflows): its send email step carries its own content written inside the step, so it does not send a saved template. ## Next Set the send up in your code with the [Templates API](/api/templates), or build an automation instead in [Build a workflow](/guides/workflows). --- # Take a tour of the app Source: https://docs.maxclicks.ai/guides/tour # Take a tour of the app Follow the work from customer data to audience, message, journey and results. This tour shows where each part lives inside your **space**, and where maxinja works alongside your team. ### Meet the layout Every screen has the same shape. The **top bar** takes you between areas. **maxinja**, your assistant, sits on the left and you can talk to it at any time. The rest of the screen is where you view and work on your data. ### Records This is your people and your things. Open **Records** in the top bar and pick a type to see everyone in your space, search them, add them one by one, or import a list. Each contact holds details like name, email, and anything else you track. ### Segments A segment is a named group of contacts, like "customers who bought last month". Open **Segments** to build these groups by describing who you want in plain words. You use them to target emails. ### Broadcasts and Templates These are how you send email. A **Broadcast** is a one-time email to a group, like a newsletter. A **Template** is a saved email you reuse, like an order confirmation. Both use the same visual editor. ### Workflows A workflow is an automation that runs on its own, like a welcome email that goes out when someone signs up. Open **Workflows** to build these as a flow of steps on a canvas. maxinja builds one from a description. ### Forms Open **Forms** to build signup forms you can add to your website. Completed submissions create or update contacts. Opted-in submissions wait for confirmation when double opt-in is enabled. ### Metrics Open **Metrics** for how your email is doing: delivery, opens, clicks, and your top performing sends. ### Brand Kits Open **Brand Kits** for the colors, fonts, images, and social links your emails use, so everything you send looks like you. See [Set up your brand](/guides/brand). ### Settings and switching spaces The space menu in the top bar takes you to **Settings**, and to **Members** and **Billing** if you are an admin. Settings is where domains, sender profiles, schemas, and your brand details live. If you run more than one business, the space menu is also where you switch between spaces. What you can open here depends on your role. To find something, ask maxinja: "Where do I add new contacts?" It points you to the right area, and it can build templates, broadcasts, and workflows for you. ## Connect your systems Open **Settings → Developers** for API keys, webhooks and SMTP. The walkthrough shows each tab, including the JSON template payload required for SMTP. Start with the connection details in your own workspace. ## Make the workspace comfortable Under **Account settings → Appearance**, follow your system theme or choose Light or Dark. This preference changes the application interface, not your email’s brand palette. ## Next Head to [Add your contacts](/guides/contacts) to bring your people into maxclicks. --- # Activate new signups Source: https://docs.maxclicks.ai/guides/workflows # Activate new signups Follow up with new signups who have not created their first project. This guide uses Loopwell, a fictional project management product: wait two days after signup, check that person’s own projects, and email them only if they still have none. You will build a reviewable draft with a clear decision: **started → finish; not started → send the first-project email**. maxinja can create the steps from your brief. You check the customer match, the query, the branch and the email before publishing. ## Before you start * A contact schema for your users, a Project object schema, and a relationship identifying who created each project. A shared team account is not enough to establish ownership. * A signup event linked to the user it describes. In this example it is named **Usersignedup**; select your own equivalent in the trigger. * A [brand kit](/guides/brand), [authenticated sending domain](/domain), and email subscription/topic eligibility for the contacts who will receive marketing email. * At least two representative contacts to inspect: someone with no own projects and someone who already created one. Include a teammate’s project to check that it does not count as the user’s work. If you still need the data model, start with [Set up your data](/guides/data). The field names in this guide describe the example; maxinja must use the actual relationships in your workspace. ## The journey you will build | Stage | Customer decision | | ------------ | ---------------------------------------------------------------------- | | Signup event | Which person just signed up? | | Find Records | Resolve that person’s contact for later steps. | | Delay | Give them two days to get started. | | Query | Count projects created by this person now. | | If/Else | End the follow-up if that count is greater than zero. | | AI | Prepare first-project guidance from the person’s role and stated goal. | | Send Email | Use that guidance in the email to eligible contacts. | Query answers a factual question: how many projects exist? AI interprets the person’s goal to prepare guidance. The example keeps those jobs separate. If you only need personalized wording inside an email, you can use [Adaptyle](/adaptyle) in the email instead of the separate AI step. ### Start a new workflow Open **Workflows** in the top navigation and choose **New workflow**. Name it “First project follow-up”. Ask maxinja: “Build an unpublished workflow for Usersignedup. Find the user who signed up. Wait two days, then Query the number of projects created by that user, not their teammates. If the count is greater than zero, end the journey. Otherwise add an AI step that suggests a first project from their role and stated goal, then use its result in a marketing email. Keep it unpublished for review.” Inspect the resulting canvas. The following steps describe the settings to check or configure yourself. ### Pick what starts it Choose **Event Fired**, then the signup event under **When a … fires**. Confirm that its user relationship points to the person who signed up. Follow it with **Find Records**, selecting your user/contact schema. Ask for only the contact identified by this event, with a nonempty individual user identity that can be matched to a project creator. Enable **Process each record separately** so later steps receive one contact. This lookup should match exactly one person; a broad filter can start multiple follow-ups from one signup. ### Wait, then check their own projects Use an outgoing connection’s plus button to add **Delay**. Describe “Wait two days” and send the instruction to maxinja. Review its compiled explanation. Add **Query** after the delay. Describe “Count Project records whose creator is the user from this signup event. Return zero when this user has no projects. Do not count projects created by other people in their account.” Ask maxinja to name the result `projectCheck`, with a numeric `projectCount` and a boolean `hasProject` that is true only when the count is greater than zero. Inspect **Compiled**, **Depends on** and **Reads**: they should identify the Project schema and the creator relationship, using this run’s user. The earlier user lookup must exclude a missing user identity. A count of zero only has meaning for an identified person. A failed query is not a zero count. ### Stop when the person has already started Add **If/Else**. Describe “projectCheck.hasProject is true” and compile it. Leave **If true** unconnected so that path ends. Connect **If false** to the AI step below. Check the wording and connections together. Reversing this condition would email the people who have already created a project. ### Prepare first-project guidance Add **AI** on the false branch. Under **Expected data**, select the user’s role and stated goal. Set **Prompt** to suggest one first project using only that context, with a short generic suggestion when context is missing. Do not ask the model to guess whether the person has already started; Query has answered that. Ask maxinja to configure a **Result shape** containing a text field named `suggestion`, and name the step’s output `firstProjectGuidance`. Choose both **Model** and **Thinking** under **AI settings**. Web search and workspace data tools are unnecessary when the required facts are already supplied. The result is guidance that later steps can reuse. This step does not send an email. ### Set up the email Set **Sends to** to the single user/contact found earlier. Open **Email template**, choose your editor and **Marketing** sending mode, then compose the first-project email with your brand kit. The template belongs to this step. Insert the AI result with [Liquid](/liquid), using `{{ firstProjectGuidance.suggestion }}` for the output configured above. Keep the project-creation link in a fixed button. Check that the email’s data includes the AI output, then preview its layout using representative sample guidance. A content preview does not execute the preceding workflow steps. Check the sender, subscription topic and recipient data. A message accepted by the email provider is not confirmation of delivery. ### Publish to turn it on Review the decision checks below and [verify a controlled run](#verify-a-controlled-run) before publishing the customer-facing version. Resolve blocking issues and read warnings. Publication makes this version live for qualifying signup events; it does not replay all historical signups. If you are editing an existing workflow, its previous published version can still run while you change the draft. Publishing also resumes a paused workflow, including waiting runs on their original versions. ## Review the customer decision These are the expected outcomes to check, not results from the recorded draft: | Customer situation when Query runs | Expected behavior | | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | No projects created by this person | Query returns a zero count; If false reaches AI and then the email. Sending still requires eligibility. | | At least one project created by this person | Query returns a positive count; If true ends the journey. | | A teammate created a project, but this person has none | Count remains `0`; their teammate’s work does not stop this follow-up. | | Signup cannot resolve to a contact with a usable user identity | Find Records produces no match and that fan-out ends. Inspect the event/contact mapping. | | Query cannot produce the declared result | Inspect the failure; do not substitute zero for missing evidence. | The Query runs after the delay, so it can see projects created during those two days. Its result describes that lookup; it is not a guarantee that nothing changes between the query and the email. If your source of truth is a recorded activity rather than Project records, consider **If/Else → Customer event history**. It has a defined lookback, customer/correlation matching and late-event allowance. That is a different decision from counting current records; see [event-history decisions](/workflows/ai#decide-from-customer-event-history). ## Verify a controlled run An email preview checks the message with sample data. To check the lookup and branch, execute the journey for test contacts: 1. **Duplicate** the workflow from its list menu and name the copy clearly. Keep the customer-facing original unpublished. 2. Restrict the copy's trigger to your test contacts. Remove its **Send Email** step while checking the decision; check that no other record changes or external actions remain. Shorten the test copy's delay and compile that change. 3. Publish the copy, then create a new qualifying signup event for each test contact. This is real execution and uses credits. Publication alone does not replay their old signup events. 4. Open **Runs**. Compare the resolved contact, Query's **Received** and **Produced**, and the If/Else path with the table above. For the zero-project contact, inspect the AI suggestion and repeat with a missing goal to check its generic answer. 5. Pause the copy after the checks. Apply any corrections to the original, preserve its intended two-day delay, and check the final email separately with **Send test** before publishing to customers. Success is evidence for both paths: the customer with a project finishes without a reminder, while the customer without one reaches suitable guidance. The test copy's completed run does not establish that the original workflow has sent email. ## Review the work it can create Check the contact lookup and any **For Each** list. One trigger can create many child runs, each executing later steps and consuming usage. This journey should resolve one user per signup. An empty list ends a fan-out; a list retained in one run can still reach the next step. For **Send Email**, confirm its recipient path is one contact, its marketing/topic eligibility, its sender and a representative render. Its **Then** branch follows provider acceptance; its optional email-event branch starts new runs when the selected activities occur. After publication, inspect the first qualifying examples in **Runs**. Compare **Received** and **Produced** values for Query, the If/Else path and the email activity. [Run history](/workflows/run) tells you where a journey waited or failed. Keep initial enrollment narrow while validating the journey. Measure the intended result separately: did emailed users subsequently create their own first project? Email delivery and clicks are useful diagnostics, but they do not establish that activation happened or that the message caused it. ## Good to know * Existing runs keep their published steps and email templates. Ordinary record values are read as needed. Background [Scheduled evaluated attributes](/attributes#computed-attributes) use the generation pinned for the root run and its children; [evaluated attributes](/evaluated-attributes) are on-demand lookups. Sending permission, consent and credits are still checked during execution. * Standard internal workflow steps have no fixed operation fee. Compiling the count condition avoids interpreting that condition with AI on every run; the AI guidance step, data work and the email allowance remain separate. Adaptyle would add model usage if you also include it in the email. Review [workflow billing](/workflows/billing) before increasing enrollment. ## What you cannot undo **Pause** prevents new runs and parks existing execution before more steps start. It cannot undo completed writes or an email or HTTP request already in flight. **Stop new runs** stops enrollment while existing runs continue. **Terminate run** ends a selected run, its children and pending continuations. There is no skip or rewind control, and accepted effects remain. A correction affects new runs after publication. Old runs retain their old version, and publishing also resumes the workflow. Review those waiting runs before publishing a correction; do not assume they will inherit it. Credit shortage, capacity limits and temporary service waits can resume automatically. A terminal failure needs investigation. Inspect an external call or uncertain email acceptance before re-triggering the journey, because the other system may already have acted. ## Related * [Send a broadcast](/guides/broadcast): a one-time email instead of an automation. * [Work with maxinja](/guides/maxinja): the assistant builds and edits a workflow from a description. * [Workflow blocks](/workflows/blocks): the fields and behavior of each step. --- # Integrations Source: https://docs.maxclicks.ai/integration # Integrations Bring customer facts from the systems you already use into your audiences and messages. A store supplies orders and products; a billing system supplies subscription status; your application supplies accounts and activity. maxclicks can use those records to decide who to contact and what to say. Connect Attio, Segment, Shopify, Stripe, Supabase or Zapier through the provider's maxclicks integration setup. For custom connections, use the [Public API](/api) for ongoing record updates and business events, [CSV import](/101/importing-data) for existing contacts and objects, and [webhooks](/webhooks) to report activity to your own systems. Your developer maintains custom connections; your marketing team defines audiences, messages and journeys. Start with the [developer handoff](/101/developers). ## Start with the customer message For a repeat-purchase campaign, you need the customer, their past order and the products in it. Sync those records from your commerce integration, or supply them through the API or separate CSV imports into the appropriate schemas. Then use an [evaluated relationship](/evaluated-attributes) to find the right orders for each contact. Include that data in [Adaptyle](/adaptyle) or [Liquid](/liquid) before previewing the email. Verify one known customer and order first: source ID, fields, relationship and message preview. Connecting the provider alone does not establish that every historical record or relationship is ready. The [repeat-purchase guide](/guides/repeat-purchase) shows how those facts become an email. ## What you can connect Choose the integration that holds the customer facts your campaign needs. | Service | Data and workflow role | | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Shopify** | Dedicated commerce schemas for customers, products, collections and orders, plus business events; initial-sync choices determine what history is brought across | | **Stripe** | Enriched customer profiles and billing lifecycle events; subscription and invoice object backfill are optional settings, not an assumption for every connection | | **Attio** | People and company data, with email and engagement actions integrated into the CRM | | **Supabase** | A selected contact table and optional object tables; configured identity columns and mapped database columns determine the record shape | | **Segment** | A destination for the calls and mappings configured in your Segment connection | | **Zapier** | Selected triggers and actions, including record upserts, events, template sends and workflow enrollment | A provider name does not mean every upstream object or historical event is imported. Check the connection's actual mapping, toggles and initial-sync scope. For example, Stripe's customer backfill and subscription/invoice object backfill have separate choices; Supabase requires a contact table and identifier mapping rather than choosing an arbitrary table automatically. ## Connect one Start from the provider's maxclicks integration setup or app directory. Follow its authorization flow and sign in to maxclicks when requested. Choose the workspace you intend to connect; integration authorization requires administrative access. Complete any upstream authorization and mapping choices, then return to maxclicks to verify the resulting connection. A successful connection establishes authorization; it does not prove a particular historical record has arrived. Inspect a known customer and related object, then compare its identity and business fields with the source. Check the provider's synchronization status and errors before relying on missing records as evidence that a customer has no activity. A workspace has one connection entry per provider. Review the provider's account or store selection before connecting another account, and use the integration's setup information for supported account scope. ## What the integration owns, and what stays yours An integration can create dedicated schemas, attributes and other definitions such as segments, webhooks or a brand kit. **Managed by** and lock indicators identify those definitions. The owning integration maintains them; manual schema configuration and CSV import are restricted where they would conflict with that ownership. The integration controls the entire attribute catalog of an integration-owned schema. You cannot add, edit or delete its attributes through the app, including a field without its own ownership badge. Keep team-maintained context in a separate native schema and connect it with an [evaluated relationship](/evaluated-attributes). Contacts and objects remain records you can inspect and use in audiences, emails and workflows. Manual edits to an upstream-owned value can be overwritten by the next sync. Deleting a record in maxclicks does not establish that the upstream system has deleted it or will stop supplying it. Identity is still scoped to each schema. An integration's customer schema does not automatically merge its people into your existing default contact schema. Choose the right schema for the campaign and establish explicit relationships when information spans schemas. ## Disconnecting, and the difference that matters Disconnecting a provider stops its connection while preserving its existing maxclicks data. This requires administrative access. The connection is removed, ownership locks are cleared, and the existing schemas and records remain under your team's control. Uninstall or provider erasure is a different operation. The integration's erasure flow requests deletion of the data it owns, including records in its schemas. That cleanup is durable and can continue asynchronously; workspace processing can be gated while it completes. Dependent campaigns and definitions can be affected by losing those schemas. Choose based on the intended result: preserve the model and stop updates with Disconnect, or follow the provider's erasure flow when the owned data should be removed. Reconnecting later should be reviewed against the preserved schema and provider mapping rather than assumed to repair every previous campaign reference. ## Inspect the complete connection For a repeat-purchase campaign, check the following before expanding the audience: 1. Find one customer you can also inspect in the source system. Compare the source identifier and the selected contact schema. 2. Find one related order. Check its amount, currency and current status, rather than assuming a connection imports all history. 3. Evaluate the customer's order relationship. A saved order does not establish a relationship until its matching identifiers and lookup agree. 4. Preview the message with that customer's data and inspect any missing fields. 5. Check one qualifying customer, one excluded customer and one customer with no order history in the segment. If a record is missing, inspect mapping and sync scope first. If the record exists but the relationship is empty, inspect the join identifiers. If the audience is correct but the email lacks the order, inspect the email's requested attribute expansions. These are separate decisions with separate failure causes. ## Pushing data in without an integration Use the [Public API](/api) when your backend owns record updates or business occurrences, and [webhooks](/webhooks) to receive supported notifications from maxclicks. For typed browser or server observations, configure [collected events](/events#collected-events). Agree which system owns each field, how customers are identified, and how retries and consent are handled. Give your developer the [HTTP quickstart](/quickstart) to verify the connection before sending production traffic. --- # Sending to Large Audiences Source: https://docs.maxclicks.ai/large-audience # Large audiences A large send is a sequence of decisions: who qualifies, which data the message uses, whether the rendered content is ready, and whether each recipient remains eligible for delivery. A segment count answers only the first question. Use a [broadcast](/broadcast) for one campaign sent to a selected contact audience. Prepare the audience and content together, then inspect the send's actual progress and recipient outcomes. For a seasonal launch, first review a frequent buyer, an occasional buyer and a customer with sparse history. The useful result is a campaign whose audience rule includes the intended customers, whose content handles each case, and whose estimate you can assess. [Send a broadcast](/guides/broadcast) provides the setup steps; this page explains what changes as the audience grows. ## Pick the audience Choose the **Contact schema** and an optional saved **Segment** when creating the broadcast. Review the **Audience** tab afterward, or generate a custom filter for this campaign. No segment or filter means the whole selected schema, so make that choice deliberately. Check known inclusions and exclusions in the segment preview. A count can change as source records change before preparation. The send later seals its selected record membership and destination addresses; subsequent segment changes do not continuously add recipients to that broadcast. The preparation step deduplicates both contact identities and normalized destinations within the broadcast. Multiple records pointing to the same delivery address therefore do not imply multiple messages for that send. ## Send it, or schedule it Complete the email, sender, subject and audience, then review readiness and schedule or start the broadcast. The send captures a revision of its content, brand settings, audience rule, data definitions and quality policy. Editing a separate source after that does not silently rewrite the revision already being processed. Preparation and content checks happen before recipient delivery. A large campaign can still be preparing or checking content even though its send time has arrived. Review the visible phase and any reason it is waiting instead of repeatedly starting another copy. Before preparation starts, **Cancel send** returns the scheduled campaign to an unscheduled state. After preparation starts, **Stop sending** cancels eligible unsent work; an email already in flight may still arrive. A finished, non-stopped campaign can offer **Retry** for failed recipients with no attempted send, or **Close remaining work** to end that work. This does not resend accepted or unconfirmed messages and cannot resume a stopped campaign. Use the [broadcast lifecycle reference](/broadcast#retry-eligible-failed-recipients) for the action available in the actual state. ## What changes at volume A large audience increases rendering, lookup and delivery work. If the space runs out of credits, the current broadcast pipeline pauses for credits; waiting recipients are not all converted into terminal failures just because the balance reached zero. Review the reason and billing state before treating a pause as failed delivery. Select [Evaluated fields](/evaluated-attributes) in the email's **Data** tab. On-demand relationships perform their lookups for the recipients that need them; unnecessary nested relationships multiply that work. Narrow lists in their definitions and give missing values meaningful content fallbacks. Scheduled Evaluated fields read published background results. A broadcast revision captures its selected generation, freshness policy and observation time when that context is first established. A later refresh does not replace the selection halfway through the revision. Enabled fields with no complete or expired publication block expansion; paused fields can retain the captured last-published policy. A valid publication with no result for a particular contact can return null. Verify **Published value**, **Data through** and the status before starting a campaign that depends on recent event history. Use the current [billing and usage reference](/billing) for plan allowances and metered work. Audience size alone does not describe the cost of AI generation, nested data reads or the complete delivery path. ## Watch it go out Follow the broadcast's phase and readiness or failure reason. Keep campaign-level preparation separate from the result of each email. * **Metrics** shows campaign timing and aggregate delivery and engagement. Use **Refresh** to request updated metrics. * **Emails** shows recipient rows and their current status. Open a row to inspect the message and its activity. * A credit pause, content-quality pause or campaign failure needs a different response from one recipient's bounce, rejection or suppression. A segment match is not the final send count. Marketing eligibility also checks email presence, channel subscription and the chosen topic; applicable suppressions and current delivery conditions can prevent dispatch. An individual email’s **Sent** status means provider acceptance; delivery and engagement arrive as separate feedback. Campaign funnel totals have a different scope; see [report definitions](/guides/reports#read-the-rate-and-its-denominator). When a recipient's outcome is uncertain, inspect that specific attempt. Starting a second broadcast or changing occurrence identifiers can create another delivery intent; it is not a diagnostic refresh. The [Broadcasts API](/api/broadcasts) exposes the supported campaign operations and status data. ## Before you send to a big list Verify the sending [domain](/domain), test representative data and missing-value cases, and inspect the campaign's readiness checks. A small live audience can help validate source data and resulting messages before a wider campaign, but it has its own delivery and consent consequences. After sending, review delivery feedback and [Postmaster](/postmaster) alongside campaign engagement. Use the evidence to improve the next audience or message rather than treating opens as proof that every selected contact received it. ## Related * [Broadcasts](/broadcast): the complete send lifecycle and recovery controls. * [Segments](/segments): the audience definition and preview. * [Attribute expansion](/attribute-expansion): data selection and generation snapshots. * [Templates](/templates): reusable messages invoked for individual recipients. --- # Link tracking Source: https://docs.maxclicks.ai/link-tracking # Link tracking Link tracking adds campaign labels to eligible links in your marketing emails. When a customer follows a tagged product link, those labels travel to the destination URL. Your website's analytics can use them to identify campaign traffic. For a coffee announcement, you might use `spring-launch` as the campaign, `maxclicks` as the source and `email` as the medium. Set the workspace's source and medium once, then choose the campaign on each email. Verify the final URL and your website's analytics before interpreting the result as attributed traffic or revenue. ## Separate URL tagging from email click metrics | Evidence | What it describes | Where to inspect it | | ------------------------------------------ | -------------------------------------------------------------------------------------- | ----------------------------------------------------------- | | `utm_campaign`, `utm_source`, `utm_medium` | Labels included in a destination URL. | The prepared email's link and the destination URL. | | Email click activity | Click events reported by the email provider and recorded against an email. | The email's **Activity** and campaign or workspace metrics. | | Website visits or purchases | What your website analytics and business data record after the destination is reached. | Your analytics and order or product system. | The workspace's UTM switch does not turn provider click reporting on or off. An email can have click activity without these added labels. A tagged link does not prove a visit was recorded, a purchase occurred, or the campaign caused that purchase. See [email results](/guides/reports) for the distinction between delivery, clicks and your customer outcome. ## Configure workspace tagging Open **Settings → Link Tracking**. The setting applies across the space, rather than only to the email currently open. The screen is reachable by space members, but saving workspace tagging changes requires admin permission. Editing the email's campaign requires write permission. ### Enable marketing-email tagging Turn on **Tag links in marketing emails**. The switch is off by default. **Source** and **Medium** appear when it is enabled. ### Choose source and medium Enter the values your reporting convention needs, such as `maxclicks` for **Source** and `email` for **Medium**. These become `utm_source` and `utm_medium`. Empty fields remain unset; the displayed placeholders are not saved default values. ### Check the saved settings Edits save automatically. Wait for the saving status to settle and inspect any error in the page header. The example under **A link in your next campaign** illustrates the current fields; it is not evidence that an email was sent or that analytics received a visit. Source and medium are optional trimmed strings, and neither field automatically becomes the placeholder when left blank. Use consistent explicit values across campaigns when your reporting needs a common source and channel. Turning the switch off prevents automatic tagging on subsequent preparation; it does not remove tags already authored into your links or change delivered emails. ## Set the campaign on an email In the email composer's setup controls, find **Link tracking** and enter **Campaign**. This value becomes `utm_campaign`. The field appears when workspace tagging is enabled and the email uses **Marketing** sending mode. Leave **Campaign** empty to derive a name from the owning broadcast, reusable template or workflow step. A workflow uses the Send Email step's label, falling back to the workflow name. The fallback is lowercased, accents are normalized, and punctuation or spaces become hyphens: `Spring coffee launch` becomes `spring-coffee-launch`. An explicit campaign value is used as the saved text rather than converted into that owner-name slug. URL encoding handles characters that need escaping. Use a stable value when later title edits should not change the campaign label. An empty campaign and an unnamed owner can leave `utm_campaign` absent. A name that produces no letters or digits also has no fallback. Set **Campaign** explicitly when that label is required for your reporting. The composer shows the workspace source and medium and offers **Change them** to open workspace settings. Those controls change the whole space. Existing email-level source or medium values, when present, take precedence over workspace defaults; the current composer exposes the campaign field rather than separate source and medium editors. For a workflow email, draft changes take effect after workflow publication. Existing runs keep their published email configuration. See [workflow versions](/workflows/manage). For reusable content and the composer, see [templates](/templates) and [the email editor](/editor). ## Understand which links change Automatic tagging resolves the campaign, source and medium while preparing the final email. It changes absolute `http://` and `https://` links in the rendered HTML body. It does not create a website analytics integration. | Link or setting | Result | | ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | External absolute HTTP or HTTPS link | Receives each nonempty resolved UTM value. | | Existing `utm_campaign`, `utm_source` or `utm_medium` | Replaced only when that key has a nonempty resolved value. | | Existing UTM key with no resolved value | Its authored value remains. Blank settings do not remove it. | | Other query parameters or a fragment | Preserved, including values such as `variant=espresso` and `#details`. | | maxclicks app, API or platform-domain links and their subdomains | Excluded from automatic tagging. This includes maxclicks-managed unsubscribe and preference links. | | Relative, malformed, `mailto:` or `tel:` link | Left unchanged. | | Transactional email or disabled workspace switch | No automatic UTM tagging. Existing authored URL parameters remain. | The link's label does not determine exclusion. An external preference or unsubscribe URL is still an eligible HTTP(S) link if its host is outside the platform exclusions. Inspect any custom preference links rather than assuming text such as “unsubscribe” makes the link exempt. Automatic tagging happens after rendering. A personalized or Liquid-generated destination must resolve into a valid absolute HTTP(S) link to receive the tags. The final plain-text body is generated from the tagged HTML, including its link destinations. ## Inspect an example URL With tagging enabled, **Campaign** set to `spring-launch`, **Source** set to `maxclicks`, and **Medium** set to `email`, this authored link: ```text https://shop.example/products/coffee?variant=espresso#details ``` becomes: ```text https://shop.example/products/coffee?variant=espresso&utm_campaign=spring-launch&utm_source=maxclicks&utm_medium=email#details ``` If the original already contained `utm_source=old`, the nonempty resolved source replaces it with `maxclicks`. An unrelated `utm_content=button` remains. If no source is resolved, the old authored source remains instead. Query-parameter order or URL encoding can change without changing those values. ## Verify a prepared marketing email An editor preview checks the content and destinations. **Send test** sends the supplied preview HTML as a transactional test; it does not run automatic marketing UTM resolution. A test email can retain tags you authored, but it is not proof of the production tagging path. Use a narrowly scoped marketing send to contacts you control when you need to check final automatic tagging. Sending still requires a verified sender, eligible recipients and available sending allowance. Follow [sending your first email](/quickstart/send-your-first-email). ### Check the configuration Confirm the workspace switch, explicit source and medium, email sending mode, and campaign value or owner name. Inspect a representative rendered link, including any existing query parameters. ### Inspect the actual email Open the prepared or sent email's details and inspect its destination link. Confirm the expected UTM values and preserved product parameters. Provider tracking can wrap the link; inspect the destination reached as well. ### Check the destination evidence Follow the intended link and inspect the landing URL. Check whether your website analytics receives the expected labels. Read email **Activity** separately when checking the provider's click event. Broadcast preparation captures the resolved UTM settings in the send revision. A later workspace setting change does not rewrite that captured revision or delivered content. Workflow and reusable-template sends retain their prepared artifact for safe recovery. Verify the actual email rather than assuming today's settings describe every earlier message. ## Find a missing or unexpected label | What you find | What to inspect | | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | Workspace settings failed to save | Admin permission and the page's saving error. Being able to open the page does not grant permission to update it. | | No added tags | Workspace switch, sending mode, absolute HTTP(S) destination and excluded platform hosts. Check whether you inspected a preview or transactional test. | | No campaign label | Explicit **Campaign**, owning item's name and whether its fallback produces a nonempty slug. | | An old source or medium remains | A blank resolved value preserves an authored key. Check existing email-level values before workspace defaults. | | A recent change is absent from an email | Its captured broadcast revision, published workflow version or already prepared artifact. | | Tags reach the page but reporting differs | The destination's redirects and analytics setup, plus the exact labels received. URL tagging alone does not establish successful attribution. | Correct the relevant saved setting or authored URL, then inspect newly prepared content. Changing settings does not edit an email already delivered. Do not resend a customer campaign solely to repair reporting labels; use the existing email and destination evidence when assessing what happened. ## Related * [Email editor](/editor): content, sending mode and campaign controls. * [Broadcasts](/broadcast): audience, preparation and delivery state. * [Templates](/templates): reusable email configuration and sends. * [Workflow management](/workflows/manage): published versions and existing runs. * [Email results](/guides/reports): provider activity and business-outcome limits. --- # Liquid Source: https://docs.maxclicks.ai/liquid # Liquid Liquid inserts supplied facts into an email: a customer name, order total, renewal date or destination link. Use it for values and conditional content that should follow defined rules, alongside [Adaptyle](/adaptyle) for passages that need generated writing. That is the difference from [Adaptyle](/adaptyle), which writes new copy for each person. Liquid fills in what you know. Both work anywhere in a [template](/templates), a [broadcast](/broadcast), or a workflow's send-email step, and they mix freely in the same email. ## Insert your first value Wrap the value in double braces: ```liquid Hi {{ contact.firstName | default: "there" }}, your order is on its way. ``` `contact` there is a key, not a fixed word. It is right only when your email's key for the recipient really is `contact`, and the next section is where that key comes from. With `firstName` set to `Ada`, the preview reads “Hi Ada, your order is on its way.” With an empty name, it reads “Hi there, your order is on its way.” Select both samples in **Data** and inspect **Base** preview. Liquid needs no writing-model call to make this substitution. ## What you can reference An email declares what information it needs, and you address each piece by the key on that list, not by its schema name. Where the key comes from depends on what is sending the email: * On a [template](/templates) or a workflow send-email step, you name it yourself when you add the property. * On a [broadcast](/broadcast), maxclicks names it for you, after the audience's contact [schema](/schemas) slug. A `Learner` audience gives you `learner`, so you write `{{ learner.firstName }}`. A schema slugged `abandoned-cart` initially gives you `abandoned_cart`. Once saved, the email retains that key when the schema is renamed. Open the **Data** tab in the editor if you are unsure: the key is on the row. The examples on this page use `contact`, standing in for whatever your key actually is. A record gives you its stored writable [attributes](/attributes). [Evaluated fields](/evaluated-attributes) require explicit expansion. On-demand fields resolve their lookup; scheduled fields read a published result without forcing a new calculation. An enabled calculation with no complete result or an expired result blocks expansion, rather than silently rendering empty. A valid publication with no result for this record can be null, and paused definitions can retain their captured last-published policy. Active broadcast revisions and workflow root runs retain their selected generation and freshness policy; a newer publication does not replace that context. See [expansion freshness](/attribute-expansion#know-how-fresh-the-value-is). ### Contact fields Every contact carries these, plus your own attributes. | Field | Type | | ----------------------------------- | ------------- | | `id` | string | | `fullName`, `firstName`, `lastName` | string | | `email` | email | | `phone` | phone | | `userId` | string | | `avatarUrl` | url | | `notes` | string | | `tags` | string list | | `subscriptions` | string list | | `topicIds` | string list | | `source` | string | | `emailStatistics` | object | | `createdAt`, `updatedAt` | date and time | `emailStatistics` holds `sent`, `delivered`, `bounced`, `complained`, `unsubscribed`, `uniqueOpens` and `uniqueClicks`. ```liquid {{ contact.firstName }} {{ contact.emailStatistics.uniqueOpens }} {{ contact.company }} ``` `contact.company` only resolves if `company` is a field you added to that contact schema. ### Objects, events and plain values Objects and events give you their fields under the key you gave them. A plain JSON value gives you whatever its shape defines. ```liquid {{ order.total }} {{ purchaseEvent.amount }} {{ customData.nested.field }} ``` ## Give it a fallback A misspelled field does not fail the send. It renders as nothing, so `{{ contact.frstName }}` quietly ships an email that starts "Hi ,". Always give anything that might be empty a fallback. ```liquid Hi {{ contact.firstName | default: "there" }}, ``` The editor catches the typo before you get there: it checks every expression against the information the email says it needs, flags a field or a filter it does not recognise, suggests the closest match, and offers a one-click fix in the visual editors. Once the template has sample data it also shows you what each expression resolves to, and warns when one comes out empty. The MJML and HTML code editors underline the same problems in place. ## Let maxinja finish the expression In the two visual editors, keep typing inside a Liquid expression and maxinja offers the rest of it as faded text a moment after you pause. `Tab` accepts it, `Escape` dismisses it, and typing the same characters yourself eats into it rather than dismissing it. It waits until there are a few characters to go on, so a bare `{{ }}` gets nothing. Each suggestion is a small AI call, so it is a workspace-wide choice rather than a per-email one. Find it under **Settings**, **maxinja**, on the **General** tab, as **Editor autocomplete**: **Automatic** suggests as you type, **Off** means no suggestions and no cost. ## Show something only when it applies ```liquid {% if contact.firstName != blank %} Hello {{ contact.firstName }}! {% else %} Hello there! {% endif %} ``` | Operator | Meaning | | -------------------- | -------------------------------- | | `==` | Equal | | `!=` | Not equal | | `>`, `>=`, `<`, `<=` | Numeric comparison | | `contains` | Contains this text, or this item | | `and`, `or` | Combine conditions | `unless`, `elsif` and `case`/`when` work too. ```liquid {% case contact.userGroup %} {% when "premium" %}Welcome, Premium Member! {% when "trial" %}Enjoying your trial? {% else %}Welcome! {% endcase %} ``` ## Repeat a block for every item ```liquid {% for item in order.items %} {{ forloop.index }}. {{ item.name }} - ${{ item.price }} {% else %} No items. {% endfor %} ``` Add `limit:3`, `offset:1` or `reversed` to control what you loop over. Inside the loop, `forloop.index`, `forloop.index0`, `forloop.first`, `forloop.last` and `forloop.length` tell you where you are. ## Reshape a value with filters Pipe a value through a filter with `|`, and chain as many as you like, left to right. ```liquid {{ contact.firstName | upcase }} {{ contact.notes | truncate: 50 }} {{ contact.tags | join: ", " }} {{ order.total | round: 2 }} {{ contact.createdAt | date: "%B %d, %Y" }} {{ contact.email | url_encode }} {{ contact.fullName | default: "Customer" | capitalize }} ``` | For | Filters | | -------- | ---------------------------------------------------------------------------------------------- | | Text | `upcase`, `downcase`, `capitalize`, `strip`, `strip_html`, `truncate`, `replace`, `url_encode` | | Numbers | `round`, `floor`, `ceil` | | Lists | `join`, `first`, `last`, `size`, `map` | | Dates | `date`, with codes like `%Y`, `%m`, `%d`, `%B`, `%H`, `%M` | | Anything | `default` | maxclicks uses LiquidJS for standard filters and tags, with bounded rendering and no filesystem template library. `include`, `render` and `layout` cannot load files from your project or a remote URL. Put the required email content in the template itself. ## Keep data, markup and AI instructions separate Liquid runs before Adaptyle. An authored `if` condition can include or exclude an AI prompt for a particular recipient, so one email can need AI while another follows only its static branch. ```liquid {% if contact.firstName != blank %} Hello {{ contact.firstName | escape }}, {% else %} Hello there, {% endif %} ``` Use `escape` for customer text inserted into HTML. `url_encode` encodes a query-string value; it does not replace a complete, valid destination URL. Supply absolute links in email markup. Text stored in a customer field is treated as data. Writing Adaptyle delimiters into that field does not turn it into an executable personalization instruction. Author prompts directly in the template. Avoid capturing and transforming blocks of Adaptyle syntax with Liquid filters; transforming protected syntax markers is rejected. Rendering is bounded: the Liquid source is limited to 1,048,576 characters, serialized data and rendered output to 2,097,152 characters, and rendering to a 1-second budget with memory and evaluation limits. Long loops or oversized data can therefore fail even when the syntax is valid. Narrow the data and loop length before retrying. ## Pull in your brand The [brand kit](/brandkits) you picked in the editor arrives in the email as `branding`, so you reference your colours and logo instead of pasting them in by hand and re-pasting them when the brand changes. ```liquid {{ branding.brandName }} {{ branding.logoUrl }} {{ branding.colorPalette.buttonBackgroundColor }} {{ branding.tones | join: ', ' }} ``` | Key | Holds | | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `brandName`, `brandSummary` | Your brand's name and one-line summary | | `websiteUrl` | Your site | | `logoUrl`, `iconUrl` | Logo and icon | | `values`, `tones` | Brand values and tones of voice, as a list | | `assets` | Images from the selected collection, each with a `fileUrl` | | `colorPalette` | `canvasBackgroundColor`, `backgroundColor`, `headingColor`, `textColor`, `buttonBackgroundColor`, `buttonColor`, `linkColor` | | `stylePack` | `cornersRadiusPixels`, `spacingPixels`, `headingFontFamily`, `heading1FontSizePixels` through `heading6FontSizePixels`, `textFontFamily`, `textFontSizePixels` | | `includingRule`, `avoidingRule` | The **Always include** and **Always avoid** notes from your [brand voice](/brandvoice) | ## Check it before you send The Base preview resolves Liquid against your [sample data](/editor/sample-data) without a model call. Generating sample data or a personalized Adaptyle preview is a separate AI action. Check at least one complete record and one with missing values before using the email in a live send. ## Related * [Adaptyle](/adaptyle): mark a passage for the email’s runtime model to rewrite per recipient. * [Email editor](/editor): where you write all of this. * [Templates](/templates), [Broadcasts](/broadcast) and [Workflows](/workflows): where the saved email is used. * [Contacts](/contacts): the fields a contact carries. --- # maxinja: turn a brief into a campaign Source: https://docs.maxclicks.ai/maxinja # maxinja maxinja builds campaigns and customer journeys from your brief. Describe who you want to reach, what you want them to do and which facts must stay fixed. It works with your customer data and brand kit to save an audience, email or workflow you can open and refine. For example, a coffee brand wants returning customers to try a new roast. A useful brief is: > Create a draft broadcast for customers whose latest order was at least 14 days ago. Introduce the supplied coffee using their purchase history. Keep the price and product link fixed. Use our published brand kit and leave the campaign unscheduled. The result is a saved campaign: an audience to inspect, an email to preview for different customers and a schedule you control. Follow [your first maxinja campaign brief](/guides/maxinja) to try this with your own data. ## maxinja builds it. Adaptyle personalizes it. These capabilities work together at different moments: | Capability | What it does | In the coffee campaign | | ------------------------------------------------- | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | | **maxinja** | Builds and refines saved campaign content and settings from your brief. | Creates the audience and email, including the personalization instructions. | | **[Evaluated attributes](/evaluated-attributes)** | Run saved lookup or calculation logic when a value is requested. | Find a customer’s latest order or total order count. | | **[Adaptyle](/adaptyle)** | Uses AI to write the marked content for each recipient when the email is rendered. | Relates the new roast to Marcus’s espresso purchases while keeping the offer and link fixed. | Writing a draft and generating a recipient’s email are separate operations. You can edit Adaptyle instructions yourself or ask maxinja to help write them. ## What you can ask it to build For ongoing campaign work, the chat coordinates three kinds of deliverable. A follow-up can refine the same saved work: | Ask for | What it sets up | | ------------------------- | ------------------------------------------------------------------------------------------------------------ | | A [template](/templates) | The email itself: subject, content, sender, and a rendered preview you can look at. | | A [broadcast](/broadcast) | Its name, the contact schema, the audience, the email, and the schedule. | | A [workflow](/workflows) | The steps, the connections between them, the configuration inside each step, and publishing it when you ask. | Use the dedicated [segment](/segments), [evaluated attribute](/evaluated-attributes) and [webhook](/webhooks) controls to generate their conditions. [Schemas](/schemas) and [forms](/forms) have their own setup screens. Initial onboarding also offers agent assistance with the data model and CSV mapping while setup is incomplete; that is separate from the ongoing campaign-building chat. Use maxinja to investigate before you brief a campaign. Ask "Which customers have at least two orders but none in the last 60 days?" and it can query your workspace and report the result. These data queries are read-only. A workflow it builds can separately create or update records when that workflow runs. Product questions use the built-in reference manual. ## Where you talk to it **The chat pane.** Click the maxinja button beside your work to open it, and describe what you want. The conversation stays with you as you move around the app, so one chat can go from a broadcast to the workflow that follows it. See [working with the assistant](/maxinja/assistant). **Next to a field that needs logic.** Examples include a [segment](/segments), an **Evaluated on demand** [attribute](/evaluated-attributes) under **Settings → Schemas → Attributes**, and a [webhook](/webhooks) condition under **Settings → Developers → Webhooks**. You describe what you want, maxinja generates it, and you get back a readable description of the rule (plus, for a segment, how many contacts match). A second box under it takes your refinements. **On a workflow step.** The step panel opens with a box for describing what the step should do. What you type there goes to the chat with that step already attached. **Inside an email.** The maxinja input at the bottom of the editor’s right panel follows the selected subject, block or panel and edits the saved draft. [Adaptyle](/adaptyle) instructions stay in that draft and run later when recipient content is rendered. ## Write a useful brief Start with the customer and the decision. maxinja can use your workspace definitions and query supplied records, but the source data must exist. Include facts that are absent from the workspace, and use `@` to identify an existing campaign, segment or brand kit. A useful campaign brief includes: * **Audience:** the customer behavior, time window and exclusions. * **Message:** the reason to act, offer and desired call to action. * **Fixed facts:** prices, dates, required wording and destination links. * **Scope:** whether you want a draft, a scheduled send or a published workflow. For the coffee campaign above, add an exclusion if needed: “Exclude anyone who already received this product announcement.” For an existing campaign, attach it and name the exact change. Inspect the matching contacts, sender, template and several recipient previews. A completion message describes the saved work; it does not establish that email has been delivered. ## Build an audience for one campaign A broadcast's audience picker offers every contact in a schema, or one saved segment. To define a custom condition for that broadcast, describe it to maxinja: “At least two orders, none in the last 60 days, excluding anyone who already received the September email.” maxinja builds the campaign-specific filter from the available data. Check people who should match and people who should be excluded. A matching audience count is separate from sending eligibility: consent, topic membership and sending rules still apply. For an audience you will reuse, choose **New segment** on the audience screen or open **Segments** and describe the condition there. See [create a segment](/guides/segments). ## Inspect scheduled customer signals Use a reusable [evaluated attribute](/evaluated-attributes) when several campaigns need the same customer signal, such as completed purchases in the last 30 days. **On demand** runs saved lookup logic when selected. **Scheduled** publishes complete Event-history calculations in the background. maxinja can list Scheduled evaluated attributes, inspect their status and published values, and request a refresh using the saved definition. Define or refine the attribute under **Attributes** on its owning contact or object schema through **Review** and **Apply**. The chat's inspection tools do not create a separate signal catalog. > Show the publication status and current published values for the Customer attribute `completedOrders30Days`. Tell me which generation and data window those values describe before I use them in a journey. A refresh queues background work and can reuse an already active refresh. Reading a preview or a published value does not refresh it. Check publication status and the data window in the [scheduled evaluation reference](/attributes#computed-attributes) before treating the result as current. Configure source collection and Event schemas in [Events](/events#collected-events). A workflow's **Evaluated attribute threshold** trigger uses an enabled Scheduled attribute with a usable Event source. Publication establishes a baseline; existing matching customers do not immediately enroll. Review [the threshold controls](/workflows/blocks#trait-threshold) before publishing. ## What it costs maxinja access is included without a per-seat charge, subject to workspace write permission. All AI usage is paid separately from the shared workspace wallet. There is no monthly AI allowance. A campaign brief, revision and Adaptyle preview can each add usage; the model, context and work performed affect the total. Review [AI and usage credits](/billing/credits) and representative usage before scaling. Separate building from execution when planning usage. An [Adaptyle](/adaptyle) email invokes AI for each recipient whose personalized content is rendered. Previews and regenerations also use credits. A workflow step that calls a model adds usage on each run. There is no fixed total quote for an assistant brief or fixed internal exchange fee. Actual model/tool work adds usage, including delegated work, sample generation and images. Usage already incurred remains when work is stopped or fails. An admin can inspect spend by period, category and member under **Settings → Billing → Usage**; it does not provide a per-exchange total. In the email editor, **Personalize → Estimate** provides an [estimate for recipient rendering and campaign sending](/editor#read-the-estimate-before-scaling-up). ## The model it runs on, and where it assists In **Settings → maxinja → General**, choose the default model for new chats and the app’s generation actions. To change one conversation, use the gear icon in its message box. An active request keeps the model and MCP selection it started with; new session settings apply to subsequent work. Choose a model that supports the conversation’s attachments and required tools. Existing images and PDFs remain in the conversation when you switch models; an unsupported selection is rejected rather than silently replaced. Runtime AI has its own settings. AI-calling [workflow steps](/workflows/ai) and email templates with [Adaptyle](/adaptyle) instructions each require their own model and thinking settings. Those settings determine execution behavior and do not follow a later change to the workspace default. The same tab carries Editor autocomplete. On Automatic, maxinja finishes the [Liquid](/liquid) expression you are typing in the email editor, showing the rest of it as faded text you accept with Tab, and each suggestion is a small AI call against your credits. Off turns off both the suggestions and their cost, and you write Liquid by hand. The choice covers everyone in the space. The MCP servers tab lists the MCP servers registered for the space. Register one and maxinja can call your own tools in a conversation. Both tabs are admin-only. ## Access and execution * Workspace conversations require write access. The server checks access when a conversation starts, accepts a command and executes work. Read-only access alone does not authorize a maxinja run. * It can carry out a requested publication or send without another assistant approval stage. Product validation and execution gates still apply. See [while maxinja works](/maxinja/agentic). * It is only in the app. maxinja is not part of the [Public API](/api), so nothing outside maxclicks can start a conversation with it. ## Related * [While maxinja works](/maxinja/agentic): what happens after you hit send. * [Working with the assistant](/maxinja/assistant): the chat pane itself. * [Adaptyle](/adaptyle): personalizing an email per recipient. * [Campaign briefs to use](/guides/maxinja-tasks): examples with required data and review criteria. * [Segments](/segments) and [evaluated attributes](/evaluated-attributes): reusable audiences and customer answers. * [Billing](/billing): credits and what they buy. --- # While maxinja works Source: https://docs.maxclicks.ai/maxinja/agentic # While maxinja works After you send a brief, [maxinja](/maxinja) works on the campaign, template or workflow in your workspace. You can follow its progress, answer questions and refine the saved result. This page explains what happens when a request completes, changes direction or stops partway through. ## Changes are saved as it works maxinja saves changes directly to the campaign, template or workflow it is working on. There is no separate approval stage for each assistant edit. Ask for a draft when that is the intended result. A request to publish or send can perform that action, subject to the product’s validation, permissions and execution gates. For a first campaign brief, include **“leave it unscheduled.”** For a workflow, include **“keep it unpublished.”** These describe the state you want maxinja to save. | What happened | What to check next | | --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | maxinja saved a campaign draft. | Open its audience, email and schedule. Review the sender, offer and recipient previews. | | maxinja saved a workflow. | Inspect its trigger, timing, branches and step configuration. Check whether it is published. | | maxinja reports that a send or publication was requested. | Check the campaign or workflow state and its execution history. A completed assistant response does not establish delivery. | | The request stopped or failed. | Open the saved item to see which changes completed before continuing. | ## Following what it is doing maxinja delegates campaign, template and workflow work to specialist agents. You see their jobs, progress updates and a report of what changed. A workflow specialist handles the journey and its emails together; a broadcast specialist handles the campaign and its email together. For an audience, review the contact type, the condition in plain language and the matching people. For an email, inspect the rendered samples as well as the summary. Reports can identify incomplete work or unresolved requirements; use those to guide the next request. ## Answering its questions maxinja can ask for missing information or a decision during a task. The question appears in the conversation with its own input, and that is where your answer goes: the normal message box waits until you have replied. * Some questions come with answers you click instead of typing. * Some come with a countdown, and pick a stated default if you do not answer in time. * Some ask for a file, or for you to point at something with `@`. ## One request at a time One request runs at a time. A message sent while maxinja is working steers that active request. Name the correction and the part it changes: “Keep the offer fixed; only adapt the introduction.” If maxinja has asked a question, answer in that question’s input. ## Steer, stop and retry with the saved state in mind A correction takes effect when the running assistant reaches a checkpoint. It does not undo a change that has already completed. | Action | Effect on the conversation | Effect on saved work | | --------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------- | | Send a correction while it works | Queues guidance for the active request. | Completed edits remain; the agent can use the correction as it continues. | | **Stop** | Requests interruption and clears queued follow-up input. | Completed changes and external actions remain. | | Edit and retry an earlier message | Removes later exchanges and runs the revised request. | Does not restore previous campaign, template or workflow versions. | | Delete messages | Removes part of the conversation. | Does not undo changes or recall email. | If the connection drops, reopen the chat and inspect its status and the saved item. Continue with a reference to that item and the missing change: “Finish the sender correction on this draft; leave it unscheduled.” Check uncertain send or publication outcomes before requesting the action again. If maxinja reports a transcript limit, start a new chat and attach the existing item. If it reports a full input queue, wait for the queued input to be consumed before adding another correction. ## Follow-ups keep working on the same thing Once maxinja has built you a broadcast, the next request normally refines that same broadcast rather than starting a new one. When you do want a second one, say so: "now make a separate broadcast for the trial users". Ask it questions about what it built, too. "Why does this workflow have two branches?" goes to the same specialist and comes back as an explanation, without changing anything. ## Emails it writes for you When the work is an email, maxinja writes the content, keeps a preview rendered from sample data, and looks at that preview to check its own work. It follows the selected [brand kit](/brandkits) for colors, fonts, and images. If the email needs a picture your brand kit does not have, maxinja has two ways to fill the slot, and it chooses between them itself: * **A labeled placeholder.** A stand-in that blocks out the slot at no cost. maxinja reaches for this while the layout is still moving, or when the picture is yours to supply. * **A generated image.** A real image from the image model, on brand. This one costs credits. To decide it yourself, say so in the request: “Use placeholders; I will add the images.” To replace one, add the intended picture to the kit’s asset collection and ask maxinja to use that asset. ## Check what completion means maxinja checks the saved result before reporting a completed change. A completed draft edit can still have unrelated draft issues; it does not mean the campaign was sent or the workflow was published. Confirm those states in the campaign or workflow itself, and use email or run history to inspect execution. ## When something goes wrong * **A request fails.** It is marked as failed with the reason. Nothing is rolled back: whatever was already built is still there, and a follow-up message can continue from it. * **Credits run out.** maxinja checks your [credit](/billing) balance before it starts and again as it works, so an exhausted balance stops a run partway rather than at a tidy point. Top up, then ask it to carry on. * **You do not have write access.** Workspace assistant commands and execution are refused. If access is removed during work, further execution is stopped; previously saved work remains. ## Related * [maxinja](/maxinja): what it can and cannot build. * [Working with the assistant](/maxinja/assistant): the chat pane, references, and attachments. * [Broadcasts](/broadcast) and [templates](/templates): the things it builds most often. * [Adaptyle](/adaptyle): the per-recipient personalization it writes into an email. --- # The maxinja assistant Source: https://docs.maxclicks.ai/maxinja/assistant # The maxinja assistant Open maxinja beside your campaign or workflow, attach the item you want to work on and send your brief. The conversation stays with you as you move between pages, so you can investigate an audience, refine its email and review the resulting campaign in the same chat. New to maxinja? Start with [a campaign brief you can follow](/guides/maxinja). This reference covers the chat controls; [the overview](/maxinja) explains its capabilities and costs. maxinja runs inside the app; the Public API does not start these conversations. ## Opening the pane The pane has three sizes: | Size | Behavior | | --------- | -------------------------------------------------------------------------------------------------------- | | Collapsed | Hidden; your work gets full width. | | Expanded | Side panel, the default, about a third of the window. Drag the divider to resize; double-click to reset. | | Full view | maxinja takes the whole area. Navigating to another page exits full view. | On a narrow window, maxinja opens in full view instead of a side panel. ## Chats Each conversation is a separate chat. From the pane header you can: * Start a new chat. * Open the chat list to search conversations, grouped into `Starred` and `Not Starred`. * Rename, star, or delete the current chat once it has messages. Titles are generated automatically from your first message. Chats belong to the user who started them, within that space. Your history list does not show another member's conversations. Saved broadcasts, templates and workflows have their own workspace access: a private chat can change work your teammates can open. Search matches chat titles. Rename a useful conversation with the campaign or task name, and star it for later reference. Start a new chat when the customer goal changes substantially or you need a clean attachment history. A chat does not isolate product changes: two chats targeting the same broadcast still work on the same saved broadcast. ## Writing a request Type in plain language. `Enter` sends; `Shift+Enter` adds a line. Name the item, the change and the state you want to leave it in: > Shorten this campaign’s introduction to two sentences. Keep the offer, price and button link unchanged. Leave it unscheduled. Attach the current campaign before sending. For a new campaign, include the audience and the intended customer action. See [how to write a brief](/maxinja#describe-the-outcome-not-the-data). ## Adding context maxinja is one assistant for the whole space. You point it at a specific thing by attaching a reference to your message. * `@` or `#` (or the Add context button) opens a picker for schemas, segments, templates, broadcasts, workflows, webhooks, forms and brand kits. To attach an individual contact or object, open its record page and choose **Current view**; the picker does not search all individual records. * The Current view chip attaches the page you are on. With it attached, "add a condition for recent buyers" needs no name. Workflow-step and email-part controls can attach the selected step or passage. Check the chips on the message before sending so the request targets the right item. References attach per message, so one chat can move between entities. Opening a page alone does not attach it. Inspect **Current view** and the reference chips before sending. For a follow-up, attach the saved campaign and name the part to change, such as its introduction or audience. That avoids creating another item when your intention is to revise the existing one. The attachment identifies the item, rather than taking a permanent copy of all its settings. maxinja can load its current saved state and investigate other workspace data when needed. A request such as “use the attached Order record” supplies a starting point; it does not restrict every query to that one record. ### What context is used The conversation includes previous retained messages, your attachments and referenced items. maxinja also has product reference tools, workspace data tools and the specialists that work on campaigns, templates and workflows. A data investigation reads workspace facts; a specialist can edit its saved product item. Selected MCP servers add external tools. Supply product facts that are absent from your records: the approved price, dates, destination link and required wording. Treat customer notes, files and tool results as evidence to inspect. A confident answer is not evidence that an order exists or that a campaign was delivered. ## Attaching files Attach with the paperclip, drag-and-drop, or paste. Accepted types: `.png`, `.jpg`, `.jpeg`, `.pdf`, `.txt`, up to 100 MB each. Each attachment shows upload progress and can be removed before you send. Wait until uploads finish before sending. Files remain in the conversation's history after that message, so a model selected for a later request must also support the earlier images or documents. Use a new chat for a text-only task if those files are irrelevant to it. ## During a run maxinja handles one request at a time. While it works, it: * Shows progress updates and its reply as it works. * Hands larger jobs to helper agents and reports back what each one did. See [the agentic flow](/maxinja/agentic). * Shows a preview of what it built, with a button to open it. Messages sent during a run steer that active work. Include the correction and the result it applies to. You can also stop the exchange; completed changes remain saved. If maxinja asks you a question mid-run, you answer it in the question's own input. Queued guidance appears above the composer until the active work consumes it. Several corrections can be combined into the pending guidance; they do not create independent jobs that all start at once. A correction takes effect at a checkpoint, so inspect already-saved changes when the direction changes. ## When maxinja asks a question maxinja can ask for a decision inline, often with suggested answers to click. While it waits, the main composer locks so your reply goes to the question. Some questions show a countdown and apply a default if you do not answer in time. ## Stopping and editing | Action | Effect | | ------------------- | ------------------------------------------------------------------------------------------- | | Stop | Request an interruption. Completed changes remain saved. | | Edit and retry | Revise an earlier request and replace the later conversation. Saved product changes remain. | | Delete from a point | Remove a message and later conversation messages. Saved product changes remain. | | Vote | Thumbs up or down on a finished answer. | Editing or removing conversation history does not restore an earlier campaign state, refund incurred AI usage or reverse an external tool action. Before retrying a failed publication or send request, open the product item and check whether that action completed. Continue from the actual saved state with a specific repair request. The assistant records an exchange as processing, completed, aborting, aborted or failed. **Stop** requests interruption; an aborting exchange is still settling that request. A completed exchange means the assistant finished its response, while a failed exchange includes a reason to inspect. Neither status alone proves delivery or a complete workflow run. ## Choose tools and models for the conversation The session’s AI settings choose its model and registered MCP servers. A running exchange retains the selection it started with. Change settings for subsequent work, or stop and start a new request when the task requires a different capability. An image or PDF already in the conversation remains part of its context. A model that cannot accept that modality is rejected rather than silently substituted. For connected MCP tools, review the selected server and the task you are authorizing: those tools can perform actions in the connected service, and deleting this chat will not reverse them. Workspace conversations require write access. A read-only role cannot start a workspace run merely because the question itself sounds read-only. ### Workspace defaults An admin opens **Settings → maxinja → General** to set **Model** and **Editor autocomplete**. The workspace model seeds new chats and generation actions. Existing conversations retain their own selection; use **AI settings for this session** in the composer to change one chat. **Automatic** autocomplete suggests a continuation while you write Liquid in the email editor. Accept the faded suggestion with Tab. Each generated suggestion uses AI credits. **Off** disables these suggestions for the whole space. The choice does not stop chat or change published workflow execution. The **MCP servers** tab manages the space's registered servers. In an individual chat's **MCP** menu, select the servers that task needs. Registering a server and granting it to a conversation are separate choices. AI workflow steps have their own tool grants, model and thinking settings, as do email templates that use Adaptyle. ### Model compatibility and connected tools Choose from the models currently offered in the picker. An unsupported attachment or capability causes a selection error before inference. Choose a compatible model explicitly and retry only the work that remains. Increasing model capability or thinking depth does not supply missing business facts. MCP tools run in the connected service and can have external effects. Review the registered endpoint, credentials and available tools with your administrator. Grant a server only when its tools fit the task. **Stop**, chat deletion and removal of a tool selection do not reverse an action the connected service already accepted. ## Suggestions and usage The empty chat shows starter suggestions. Selecting a suggestion fills the composer; inspect and edit it before sending. Opening the pane or checking saved suggestions does not start a paid suggestion generation. **Generate personalized suggestions (uses credits)** explicitly requests a generation using workspace context. While it is generating, **Check saved suggestions** reads its result without starting another call. An unknown generation outcome needs reconciliation, and unresolved credits stay reserved; repeatedly opening the pane does not regenerate it. Assistant work is paid from the shared workspace wallet. New briefs, revisions, delegated work, generated images and recipient previews can add usage. There is no fixed total price for a conversation and no monthly AI allowance. See [AI and usage credits](/billing/credits). ## Privacy and retained history Chat ownership controls who can open the conversation; it does not prevent the selected model or connected tools from receiving the context needed for your request. Include the files and customer facts required for the job and review external tool grants before sending. Deleting a chat or deleting messages removes that visible conversation history. It does not delete the customers it referenced or undo saved campaign work. Source-record deletion can also remove assistant-generated history that read that private data. Your submitted text can remain while generated answers and tool results are removed. Removed private exchanges are not reused as model history or replayed. If older evidence is no longer available, investigate the current source again with data you still have permission to read. Avoid treating an old answer as a current customer profile. Access is checked when work is requested and while it executes; losing workspace write access can stop an exchange. ## Recover from an incomplete request | What you find | Next action | | ------------------------------------------------------------- | ------------------------------------------------------------------------------------- | | An uploaded file is still pending | Wait for the upload or remove the attachment before sending. | | The selected model cannot accept the conversation's files | Choose a compatible model, or start a new chat with the context needed for this task. | | The request stopped after some edits | Open the saved item, attach it and ask for the missing changes. | | Publication, sending or an MCP action has an uncertain result | Inspect the product state and receiving service before repeating the action. | | History has been removed following source-data deletion | Read permitted current data again; the removed generated context cannot be replayed. | | Credits are unavailable | Inspect billing and the exchange state before retrying paid work. | For a campaign, check the audience, sender, email and schedule. For a workflow, inspect the draft, publication state and run history. [While maxinja works](/maxinja/agentic) explains how saved changes differ from execution outcomes. ## Previews When maxinja produces something to review, it opens a preview in the right of the workspace: * Audience preview: which schema it filters, the filter in plain English, what it references, and how many contacts match out of the total. * Email preview: a rendered sample to inspect. A later personalized render can use newer data and produce different wording. Attachments and generated files open there too. Switching maxinja to full view clears the preview. ## Related * [maxinja overview](/maxinja) * [The agentic flow](/maxinja/agentic) * [Broadcasts](/broadcast) * [Templates](/templates) * [Adaptyle](/adaptyle) --- # maxclicks MCP Server Source: https://docs.maxclicks.ai/mcp-server # maxclicks MCP Server Work with your maxclicks workspace from an MCP-compatible assistant. Ask it to inspect an audience, update a customer record, or use a template your team has prepared. The assistant calls the API with the permissions you granted when you connected it, or those of the API key you gave it. The server supports local stdio through the `maxclicks-mcp` npm package and remote Streamable HTTP at `https://mcp.maxclicks.ai/mcp`. Choose the transport your assistant supports and follow the [setup instructions](/ai/mcp#run-it). ## Current API coverage The tools use the [Public API](/api) for customer records, events, prepared campaigns, workflows, and webhooks. Follow each operation’s response and status fields: an accepted request can still require event readiness checks, deletion polling, or delivery tracking. ## Connect your assistant ### Add the server to your client Add `https://mcp.maxclicks.ai/mcp` as a remote MCP server in your assistant. In Claude Code, run `claude mcp add --transport http maxclicks https://mcp.maxclicks.ai/mcp`, then `/mcp`. For a local stdio setup, use the [local configuration](/ai/mcp#run-it). ### Approve the connection Your browser opens maxclicks. Sign in, choose the workspace, and choose what the assistant may do: **Read**, **Write**, and **Admin** for suppressions if it asked for it. Check where the page says it **Returns to**, then click **Allow access**. An assistant that cannot sign in can use an API key from **Settings**, **Developers**, **API keys** instead, sent in an `Authorization: Bearer` header. ### Check it landed Ask the assistant who it is connected as. It reports the workspace and the permissions it carries, which catches a connection to the wrong workspace before it writes anything. ## What the assistant can do It gets a tool for most of what the [API](/api) can do: looking up and writing [contacts and objects](/contacts), firing [events](/events), building and counting [segments](/segments), sending a [template](/templates) or a [broadcast](/broadcast) you drafted and reading back how it performed, driving [workflows](/workflows), and managing [webhooks](/webhooks) and suppressions. [Schemas](/schemas), [attributes](/attributes), template and broadcast content, domains, senders and topics are read-only through the assistant. You author them in the app, and the assistant reads what you built. Every tool acts as the person who connected the assistant, with the permissions they granted and their current role. Suppression tools require `admin`; other operations require the documented `read` or `write` permission. A missing permission returns `403`. See [authentication](/api/authentication). The [MCP server reference](/ai/mcp) is the full picture: every tool by name, the local stdio setup with a ready-made Claude Desktop config, the consent page, and how the server checks each request. Sending is real. If you ask an assistant to send a broadcast, it sends to live people, spends credits, and cannot be recalled. Connect an assistant only to a workspace you are happy for it to act in, leave Write unchecked when it only needs to read, and be specific about what you want before you say send. ## Going further * [MCP server reference](/ai/mcp): every tool, both run modes, the consent page, and how each request is checked. * [Build with AI](/ai): the other ways to point an agent at maxclicks, including the agent skills and the CLI. * [Authentication](/api/authentication): how API keys are scoped and how an app connects with OAuth. * [Rate limits](/api/rate-limit) and [errors](/api/errors): what the assistant runs into when it moves too fast or gets something wrong. Questions or problems: . --- # Members Source: https://docs.maxclicks.ai/members # Members Workspace membership gives each teammate the access their role requires. Campaign operators need editing access; administrators manage the business settings, team and billing. A person can hold a different role in each [workspace](/workspace). Admins manage people in **Settings**, then **Members** under Workspace settings. ## Pick the right role Every member holds one of three roles. maxclicks shows the role as a badge next to their name. | Role | Can do | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Member** | Work with contacts, objects, schemas, attributes, segments, broadcasts, templates, emails, domains, senders, forms, workflows, tags and topics. | | **Admin** | Everything a Member can do, plus billing, integrations and membership administration. | | **Non-paid Admin** | Read workspace information and manage billing, integrations and permitted membership roles. Cannot create or edit campaign content or execute ordinary write operations. | There is no view-only reviewer role: anyone you add can either edit or administer. If you only want someone to handle the invoice, make them a Non-paid Admin. Workspace roles control collaboration and maxinja permissions. **There is no per-seat charge.** Paid AI work uses the shared prepaid workspace wallet, with no included monthly allowance. See [billing](/billing). ## Invite someone ### Open the invite dialog Click **Invite team members** on the Members page. ### Paste the addresses **Send Invite to ...** takes as many addresses as you like, separated by commas, spaces or new lines. ### Choose a role **Invite as** offers the roles you are allowed to hand out. Non-paid Admins can create Members and other Non-paid Admins, but not full Admins. ### They join If the address already belongs to a maxclicks user, they are added to the space at once. A new address gets an invitation email instead, and the link in it is good for 14 days. Inviting or removing teammates does not change the subscription price. Assign the roles needed for their work and review access regularly. See [billing](/billing). maxclicks accepts work email addresses only. An invitation to a gmail.com, outlook.com, icloud.com or similar address is rejected with "Please use your work email address." Invite people at their company address. ## Change or remove someone The menu at the end of each row offers **Make Member**, **Make Admin**, **Make Non-paid Admin** (whichever of these your own role allows) and **Remove from space**. Three things it will not let you do: * Change or remove your own membership. Ask another admin. * Remove or demote the last person with administrative permission. An Admin or Non-paid Admin can hold that permission; a space always keeps at least one. * Hand out a role above your own. A Non-paid Admin cannot promote anyone to full Admin, and cannot change or remove a full Admin. The search box filters by name or email, and **Filter** narrows the table to one or more roles, so you can review who has each kind of access. ## Review access before assigning work A Non-paid Admin is suitable for finance or administrative oversight, but does not have the write permission required to start or chat with maxinja—even for a question phrased as read-only. Give campaign operators the Member role, and use Admin when they also need business administration. A role is workspace-specific. Review the selected workspace before inviting people, generating a key or changing billing. Removing access does not undo messages sent, records changed or published automation already configured in that workspace. For example, give a campaign manager **Member** to build and send campaigns, and a finance owner **Non-paid Admin** to manage billing without editing campaign content. Give **Admin** to the person who needs both sets of powers. A Non-paid Admin still has administrative authority; use the role for that responsibility, not for an external reviewer who should only view a campaign. Some settings combine two requirements: the settings page requires administrative permission, and saving its content requires write permission. **Settings → Brand** and **Settings → Forms Protection** therefore need full Admin access for editing. A Member can edit ordinary campaign work but cannot reach those administrative settings pages. ## AI work uses a shared prepaid balance Teammate AI work and background automation use the same purchased workspace balance. There is no per-seat allowance or signup/domain-verification credit gift. See [AI and usage credits](/billing/credits). ## API keys carry their owner's access An API key belongs to one person and works in one space, and it can do exactly what its owner can do. There are no narrower scopes. Keys are issued and revoked in the app under **Settings**, **Developers**, **API keys**. Creating one needs the Member or Admin role: a Non-paid Admin cannot. "You can only see this key once, store it safely." maxclicks keeps only a hash of it, so if you lose the key, delete it and create a new one. The **Code** column in the table afterwards is a masked preview, never the key itself. See [authentication](/api/authentication) for using a key against the API. --- # Objects Source: https://docs.maxclicks.ai/objects # Objects An object is a business thing you want to keep track of: an account, subscription, order, product or course. It has its own record and fields. Connect it to the people it matters to, then use those facts to choose an audience and personalize their email. **The contact receives the message. The object explains the context.** Maya is a contact; Studio North, the account she belongs to, is an object. Sarah is a contact; each order she places is a separate object. Neither an account nor an order becomes an email recipient. ## When to use an object Use an object when something has its own identity, changes independently, is shared by several people, or can occur several times for one person. * **Product adoption:** keep the user's identity and preferences on the contact. Store their shared account and individual projects as objects. * **Renewal reminders:** keep each subscription's status, plan and renewal date on its own object, then find the person who owns it. * **Repeat purchase:** keep each order's amount, purchase date and status separately, linked to the shopper who placed it. * **Course recommendations:** keep each course's title, schedule and availability on a Course object, ready to use with the relevant students. A simple personal preference can stay as a contact attribute. Repeated orders should not become `order1`, `order2` and `order3` fields on the person. Separate Order records let you ask for that shopper's most recent order or count all qualifying purchases. An **object schema** is the kind of thing, such as Account. An **object record** is one instance, such as Studio North. A JSON object is only a structured value inside a field; it does not create a separate business record or relationship. ## How objects connect to people Store the identifiers on both sides, then define an [evaluated relationship](/evaluated-attributes) that matches them. A field containing an ID is the input to that rule; naming it `accountId` does not create the relationship automatically. Here is an illustrative account setup. `accountId`, `accountRole`, `accountName` and `plan` are custom fields you create first; `externalId` is built in. * **Maya, a Customer contact:** `accountId` is `studio-north`; `accountRole` is `admin`. * **Daniel, another Customer contact:** the same `accountId`; `accountRole` is `member`. * **Studio North, one Account object:** `externalId` is `studio-north`, `accountName` is `Studio North`, and `plan` is `Pro`. Both people should resolve to the same Account record, while keeping their own role and communication preferences. On Customer, create an **Evaluated attribute** named **Account** with the key `account`. Describe it like this: > Return the Account whose externalId equals this Customer's accountId. Return one Account, or no record if accountId is empty or no match exists. Check that the generated definition targets Account with **one** cardinality. On Maya and Daniel's profiles, **Compute value** should resolve to the same account. A customer with no matching account should return no record. You can then build a segment of admins whose related account is on Pro: Maya qualifies and Daniel does not. In the email's **Data** panel, select `account` to make its name and plan available to the content. The shared object supplies the context; each contact's preferences still decide whether they can receive marketing email. For commerce, reverse the lookup: on Customer, define **Recent orders** to return Orders whose custom `customerId` equals that Customer's `userId`, with a completed-status filter, newest first and a useful limit. That relationship returns **many** records. See [Connecting records](/101/data-model/relationships) for the complete setup and checks. ## Create the object type Go to **Settings**, then **Schemas**, and choose **Create schema**. Set the type to **Object**, then fill in the name ("Product"), the plural name ("Products"), a slug, and a description. One type per kind of thing. Products and Orders are two types, not one. Every object belongs to exactly one type and is checked against it each time it is written, which is what your segments, emails and workflows rely on to find the fields there. Write the description for a person to read. maxinja reads it too, every time it builds a segment, an evaluated attribute or workflow logic that touches this object. ## Add your fields Open the type, go to the **Attributes** tab, and use **Create attribute** for each field you need: price, SKU, colour, launch date. The available field types are covered in [attributes](/attributes). Every object comes with these already, whatever else you add: | Field | What it is for | | ---------------------- | -------------------------------------------------------------------- | | ID | The identifier maxclicks gives it. | | External ID | Your own identifier for the same thing, from your store or database. | | Notes | Free text. | | Tags | Labels you can filter and act on. | | Created At, Updated At | Kept by maxclicks. | Two of your own fields are worth naming deliberately, because the app reads them: * A single-line **Text** field whose key contains `name`, `title` or `label` becomes the heading on the object's page, so `productName` counts. Without one, maxclicks falls back to your first single-line text field, and then to the External ID. * A **URL** field with its target set to **Image** becomes the thumbnail in the objects table. ## Give every object an External ID Supply the platform `id` to identify an existing object directly, or your source system's stable `externalId` to match within the object schema. Repeatedly upserting that external ID updates the same object. Names, prices and other custom fields do not establish identity. Without either identifier, repeated writes can create repeated records. A Product and an Order can use the same external ID without colliding because matching stays within the selected schema. Sending a platform ID that is not found does not mean “create this exact ID”; inspect the operation's error or unmatched result. No two objects of the same type can hold the same External ID. Over the upsert endpoint and from a workflow step, an update that would take one already belonging to another object keeps the old value, applies everything else you sent, and reports the change it skipped. Everywhere else the conflicting write is not applied at all: a clashing CSV row is counted as a failed row, with "Conflicting external ID." against it in the import's failure log, editing the field to a taken value in the app fails with a conflict error, and creating or patching a record over the API returns a conflict. Check those warnings and failed rows: a skipped or rejected External ID means your sync is pointing two things at one identifier. ## Get your objects in | Route | Use it for | | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | | The **Import** tab on the object type | An existing spreadsheet, with one object per row. | | The button above the table, labelled **New** and then your type's name | One object, by hand. | | The API | Your store, database or backend keeping maxclicks in sync. See [Records](/api/records). | | A connected app | [Integrations](/integration) bring their own object types and keep them up to date for you. | For CSV, download the schema's sample, fill it with your objects and upload it. Files can contain up to 50 MB and 500,000 rows. The standard importer matches writable field keys and omits unknown columns; inspect saved objects because the row summary does not show per-column warnings. Onboarding can instead plan a reviewed mapping with maxinja. The [import guide](/guides/import) covers the shared CSV rules. An object type created by a connected app is badged **Managed by** that app. Integration-owned definitions are locked and CSV import is switched off for the schema. Records remain editable, but a later sync can overwrite fields the integration owns. Keep your campaign-specific fields separate from the upstream system's fields. ## Work with your objects Objects live under **Records** in the top navigation, listed by the slug of each type. From there you can: * Search the list, and use **Select Columns** to pick which fields the table shows. The choice is remembered per type. * Click a row to open the object. Edits save as you type. * Tick rows to select them, then tag or delete them all at once. Deleting is permanent. The deletion operation can continue while related event and derived data are removed, and workspace processing can be gated during cleanup. Wait for completion. Schema deletion can also be blocked by dependencies; replace those references before retrying. ## Keep record state and event history distinct Update the object when its current state changes: a subscription becomes canceled or an order is refunded. Send a separate [event](/events) when the occurrence itself matters to automation. For profile calculations, attach the event's explicit `objectId`; an `externalId` inside its custom payload does not establish that subject link. Objects can receive [scheduled evaluated attributes](/attributes#computed-attributes), such as the latest observed usage of an account, as well as evaluated relationships. Both require expansion when an email or workflow needs their values. A missing published calculation is not equivalent to a measured zero. ## Put objects to work An object sitting in a table does nothing on its own. It earns its place the moment something reads it. * Add an [evaluated attribute](/evaluated-attributes) to your contacts so each contact points at the objects that belong to them: "the company this person works for", "the last three things they bought". * Pull those objects into an email with [Liquid](/liquid), or describe the result you want and let [Adaptyle](/adaptyle) write it per recipient. An email has to ask for related records before it can use them: see [attribute expansion](/attribute-expansion). * In a [workflow](/workflows), start a run with a **Record Created or Updated** step on an object type, look objects up mid-run with **Find Records**, or write back with **Create or Update Records**. --- # Google Postmaster Tools Source: https://docs.maxclicks.ai/postmaster # Google Postmaster Tools Use Google Postmaster Tools alongside maxclicks reporting to investigate Gmail sending health. It provides sender signals such as user-reported spam, authentication and compliance. Read these in Google's console; maxclicks does not import the dashboards. ## What each side covers maxclicks tracks email delivery and response events. Postmaster adds Gmail sender signals; its spam reporting concerns user reports and should not be read as a complete spam-folder placement rate. See [Google's dashboard definitions](https://support.google.com/mail/answer/14668346). | Signal | Source | | ----------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | Bounce, complaint, delivery, open, click events | maxclicks | | Bounce and complaint [suppressions](/contacts) | maxclicks | | Open sending recommendations (DKIM, DMARC, SPF, BIMI, bounce, complaint, IP warming, third-party feedback), raised in the app's **Inbox** panel | maxclicks | | Gmail user-reported spam and sender compliance | Postmaster | | Gmail authentication and delivery-health signals | Postmaster | | Gmail delivery errors | Postmaster | Postmaster only reports on domains that send a fair amount to Gmail each day. If you send a little, the charts stay empty. ## Setup ### Add your domain Sign in at [postmaster.google.com](https://postmaster.google.com/) with a Google account and add your sending domain. ### Verify ownership Google gives you a DNS TXT record. Add it at your DNS provider, the same place you added the records from [Domain Setup](/domain). Google shows no data until it confirms you own the domain. ### Wait for data Reporting depends on sufficient traffic and Google's reporting delay. An empty chart does not establish that every message reached the inbox. ## Use the signals to make a decision If delivery falls in maxclicks, open affected emails and compare provider errors before assuming the campaign copy caused it. If maxclicks shows Delivered but Gmail engagement changes, inspect Gmail authentication, spam and compliance signals alongside the audience and message. Align the dates before comparing reports: Postmaster uses UTC, is delayed and can omit low-volume data. Its user-reported spam metric is not an inbox-placement measurement. Treat an empty or changing chart as evidence to investigate, not permission to resend the same campaign. [Google explains these reporting limits](https://support.google.com/mail/answer/14668346). ## Which domain to register Verify the domain used to authenticate your mail and check the scope of each dashboard. Google's compliance view covers the primary domain and its subdomains, while other data may depend on the authentication domain. Follow [Google's dashboard scope](https://support.google.com/mail/answer/14668346). See [Subdomains vs root domains](/subdomain). --- # API quickstart Source: https://docs.maxclicks.ai/quickstart # API quickstart Connect your application to the customer experience your team has built in maxclicks. This guide verifies your API key, saves a contact, and sends a prepared email. The examples use cURL so you can inspect each request and response directly. ## Prerequisites For the first read request, you need a maxclicks [space](/workspace) and an API key. To save a contact, create a contact [schema](/schemas) in the app. To send the email, also prepare: | Requirement | What to check | | ---------------------------------- | ---------------------------------------------------------------------------- | | Brand and sender | Your brand details and sender are configured in the space. | | Verified sending [domain](/domain) | The DNS records have been added and the domain is verified. | | A sendable [template](/templates) | The template has a sender, renders in preview, and uses your contact schema. | | A test recipient | Use an address you control, with the consent required for the template. | Your team builds schemas, content, and journeys in the app. Your integration supplies customer data and triggers the prepared experience. You can complete the first three steps before setting up email delivery. ## Step 1: Create an API key Open your space, then **Settings → Developers → API keys**. Create a key for this integration and save the full value in your secret manager. The value is shown once. For these examples, set the key in your local environment: ```bash export MAXCLICKS_API_KEY='YOUR_API_KEY' ``` The key inherits its owner's permissions in the space. Keep it in server code and local development tools; never ship it to a browser or commit it to your repository. See [authentication](/api/authentication) for permissions and key rotation. ## Step 2: Authenticate Call `GET /v1/me`. This checks the credential without changing customer data. ```bash curl --fail-with-body https://api.maxclicks.ai/v1/me \ -H "Authorization: Bearer $MAXCLICKS_API_KEY" ``` A successful response returns `data.apiKey`, `data.user`, `data.space`, and `data.role`. Check that `data.space` identifies the workspace you intend to use. A valid key without a space binding returns `space: null`; see [space resolution](/api/authentication#space-resolution) before proceeding. Successful REST responses wrap their result in `data`. Errors use `error`. Check the HTTP status first, then the documented result fields. List responses also include `pagination`. ## Step 3: Upsert a record Find the contact schema you created in the app: ```bash curl --fail-with-body https://api.maxclicks.ai/v1/schemas \ -H "Authorization: Bearer $MAXCLICKS_API_KEY" ``` Copy its slug into `MAXCLICKS_SCHEMA`. Use `GET /v1/schemas/{schema}/attributes` to inspect the fields it accepts. Include required writable custom fields without defaults. Evaluated fields and fields with `definition.valueSource` are not caller-writable. A schema with writable AI auto-fill requires available credits and a [timed record-operation key](/api/idempotency#record-writes-with-ai-auto-fill) when missing values need generation. The example below assumes no missing AI auto-fill and sends the built-in contact email field; replace the address with one you control. ```bash export MAXCLICKS_SCHEMA='YOUR_CONTACT_SCHEMA_SLUG' curl --fail-with-body -X POST \ "https://api.maxclicks.ai/v1/schemas/$MAXCLICKS_SCHEMA/records/upsert" \ -H "Authorization: Bearer $MAXCLICKS_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: quickstart-contact-001" \ -d '{ "email": "you@example.com" }' ``` An upsert creates or updates a contact. An explicit `id` selects that record or fails with `404`. Without it, maxclicks matches `userId` first; an email matches only a contact without `userId`, and a phone matches only a contact without `userId` or email. Use one durable identity strategy across your product and email sends. Save the returned record ID unchanged and inspect any `warnings`. Use the same idempotency key and body when retrying this request. Choose a new key for a different contact or a later update. A strict create, `POST /v1/schemas/{schema}/records`, returns `409 identifier_conflict` if that identity already exists. ## Step 4: Find the template Prepare and preview your email in **Templates**, then list the templates available to your space: ```bash curl --fail-with-body https://api.maxclicks.ai/v1/templates \ -H "Authorization: Bearer $MAXCLICKS_API_KEY" ``` Copy the template ID into `MAXCLICKS_TEMPLATE_ID` and retrieve its requirements: ```bash export MAXCLICKS_TEMPLATE_ID='YOUR_TEMPLATE_ID' curl --fail-with-body \ "https://api.maxclicks.ai/v1/templates/$MAXCLICKS_TEMPLATE_ID" \ -H "Authorization: Bearer $MAXCLICKS_API_KEY" ``` Read `expectedDataRepresentation` to find the keys and schemas the template expects. The next example assumes a contact key named `contact`; use the actual key from your template and include any other required data. ## Step 5: Send it This request sends an email to the recipient in your payload. Use your test address and a template you have reviewed. ```bash curl --fail-with-body -X POST \ "https://api.maxclicks.ai/v1/templates/$MAXCLICKS_TEMPLATE_ID/send" \ -H "Authorization: Bearer $MAXCLICKS_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: quickstart-template-send-001" \ -d '{ "data": { "contact": { "email": "you@example.com" } } }' ``` The `data` object supplies the template's customer context. Records in that context are written before rendering, so pass the same contact identity you used above. You can also pass the saved record `id` inside the corresponding contact object. The API sends the stored template content; it does not accept a replacement subject or body. Inspect the result as well as the HTTP status. `templates.send` returns `data.emailId`, `data.status`, and `data.error`. Keep `emailId` and your original key. A known terminal refusal can return HTTP `200` with `status: "failed"`; repeating the key retains that result. Temporary dispatch refusal returns `429 template_send_retryable` with `Retry-After`, while an uncertain outcome returns `409 template_send_outcome_unknown`. Wait and reuse the same key for the retryable case; reconcile the uncertain case before starting another operation. `status: "sent"` means the provider accepted the send; it is not a guarantee of inbox delivery. See [template send](/api/templates#templates.send). Marketing sends require the contact's email consent and, when the template uses a topic, their subscription to that topic. Suppression checks also apply. If the API returns `recipient_not_subscribed` or `recipient_suppressed`, resolve the recipient's eligibility before sending again. ## Before you connect production traffic * Persist an [idempotency key](/api/idempotency) per supported logical write. After a timeout, keep the original key and payload; reconcile any unresolved outcome. * Handle [errors](/api/errors), including `200` responses with an operation-level failure. * Pace workers within [rate limits](/api/rate-limit), and parse both supported `Retry-After` formats. * Follow [pagination](/api/pagination) when a result spans more than one page. ## Related * [Send customer events](/api/events) * [Trigger a journey](/api/workflows) * [Send and measure a campaign](/api/broadcasts) * [Choose a client library](/sdks) * [Explore the API reference](/api) --- # Send your first email Source: https://docs.maxclicks.ai/quickstart/send-your-first-email # Send your first email Send a reviewed campaign to a small audience, then check what happened to each message. This guide takes you through the decisions that matter before your first send. Want to see personalization first? [Create one email for three different customers](/guides/first-personalized-email), then return here to send it. ## Before you begin You need a [workspace](/guides/setup), access to your sending domain's DNS settings, and permission to contact the people in your audience. Use a small group whose data and preferences you can inspect. ### Verify your sender Open **Settings → Domains**. Add the exact DNS record names, types and values shown for your domain at your DNS provider. Choose **Start verification check** and resolve the listed issues until the domain shows **Verified For Sending**. Choose a sender profile or From address using that domain. Check your email allowance; AI-generated content also needs available credits. [Connect your domain](/guides/domain) covers DNS delays and verification problems. [Billing](/billing) explains allowances and usage. ### Check the people you intend to reach Open **Records**, choose your contact type and inspect a few profiles. Confirm their email address, the fields your email will use, and their saved communication preferences. For your first test audience, [add contacts](/guides/contacts) individually or [import a list](/guides/import). An import can update existing profiles, so check its saved results before adding the rest. Audience membership does not by itself make someone eligible for marketing email. ### Build and review the email Open **Broadcasts → New broadcast**, choose the contact type and select the audience. Select **Next**, choose **Dynamic Composer** and **Marketing**, then **Next** to create the draft in **Composer**. If you already have a draft, open it. Review its condition and matching contacts. Give maxinja the audience, message and fixed offer facts. Ask it to leave the campaign unscheduled for review. Open the saved result, inspect the sender, subject and content, and confirm every button's destination. In **Data**, choose a representative customer and check the supplied fields. For Adaptyle, set **Model** and **Thinking** under **Personalize → Runs with**, then open **Preview → Personalized** and choose **Generate preview**. Compare a customer with complete context and one with missing data. Keep required prices, dates and links outside AI passages. Use **Send test** with an inbox you control. Inspect the delivered email on desktop and mobile. The test uses email allowance and sends the rendered sample; it does not establish your production audience's consent. See [personalization](/guides/personalize) for the full preview walkthrough. ### Choose when to send Resolve blocking issues in **Checks**. Open **Estimate** and review generation and sending usage, including possible corrections and reviews. An unknown estimate is not zero. Open **Send**, or **Schedule** after saving a draft. Choose **Send now**, **Schedule for later** or **Save as draft**. Check the intended audience and timing before confirming. [Send a broadcast](/guides/broadcast) explains each control and status. ### Check marketing eligibility A contact needs an email subscription and, if the campaign specifies one, a subscription to that topic. Applicable suppressions can still block sending. Review saved preferences: manual entry, imports, forms and API writes have different defaults. The matching audience count is not a delivery count. ## Know what happens after scheduling Preparation fixes the campaign revision and recipients, renders the emails and checks the output before dispatch. It can begin before the scheduled delivery time. People who match later are not added to that run; current consent and suppression still determine whether a message can leave. Before preparation begins, **Cancel send** returns the campaign to editing. After it begins, **Stop sending** permanently cancels the remaining work. Messages already being dispatched may still arrive, and incurred usage remains. A credit wait or quality hold is different from a stopped campaign. Inspect the reason before creating a replacement send. A finished campaign with eligible failed recipients can offer **Retry**, while **Close remaining work** ends that work permanently. See [safe recipient retries](/broadcast#retry-eligible-failed-recipients). For a quality hold, open **Quality review** and inspect the exact rejected email. See [reviewing a quality hold](/broadcast#review-a-quality-hold) for the available actions. ## What to do after the first send Open **Emails** for recipient outcomes and **Metrics** for delivery and response. An individual email’s Sent status means provider acceptance; Delivered is a later signal and does not prove inbox placement. The funnel’s Sent total can include pending or failed email records; [check the report definitions](/guides/reports#read-the-rate-and-its-denominator). Check failures or uncertain outcomes before trying again. Decide what customer action the campaign was meant to encourage, such as creating a project or completing an order. Opens and clicks do not prove that action happened. Compare the response with your business records or events. [See your results](/guides/reports) explains what each metric can tell you. ## Related * [Build an onboarding journey](/guides/workflows) that checks progress before a reminder. * [Personalize your first email](/guides/first-personalized-email) using one template and three customer profiles. * [Developer quickstart](/quickstart) for sending a saved template from your application. --- # Schemas Source: https://docs.maxclicks.ai/schemas # Schemas A schema defines one kind of business record and the fields maxclicks accepts for it. It gives your team, integrations and agents the same vocabulary for customers, subscriptions, products and business events. Start with a decision you want to support. A renewal journey needs the person to contact, the subscription they own and its renewal date. A repeat-purchase campaign needs the shopper and their orders. Creating schemas defines the kinds of records; importing or syncing supplies the actual people and things. * **Who could receive the message?** A Contact, such as Maya, a user at Studio North. * **What business thing gives it context?** An Object, such as Studio North's account with its plan and renewal date. * **What happened at a particular time?** An Event, such as a plan upgrade yesterday. Use one Account object for the shared account, with a relationship from each person who belongs to it. That lets the next lookup read the account's updated plan without copying it into every contact. [Objects](/objects#how-objects-connect-to-people) walks through the identifiers, evaluated rule and resulting audience. ## Create your first schema Open **Settings → Schemas → Create schema** and choose: * **Type:** Contact for people, or Object for business entities. This choice is fixed after creation. * **Name** and **Plural name:** the singular and list labels. Schema names are also used in generated definitions, so the form normalizes them rather than accepting arbitrary display text. * **Slug:** the stable URL and API identifier. This is also fixed after creation. * **Description:** what one record represents and which source owns its facts. Create business event schemas separately under **Settings → Events**. Website and server [collected events](/events#collected-events) have their own source and versioned payload definition there. After creation, open **Attributes** to add fields. Give each field a type and missing-value policy. A field description should include units, allowed meanings and the source of the value. See [Attributes](/attributes). ## Choose the right kind | Kind | What one record represents | Identity and change | | ------- | ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- | | Contact | A person your team can address | Can be created or updated; matched within its contact schema | | Object | A product, order, account, subscription or another business entity | Can be created or updated; your `externalId` makes repeated syncs refer to the same object | | Event | One business occurrence | Accepted once for an occurrence identity; prepared before downstream use; not edited into a new fact | Contacts and objects appear under **Records**, one list per schema. A person can have a record in two contact schemas. maxclicks does not automatically combine those profiles or their preferences across schemas. ## What every record already has All records have a platform `id` and timestamps. Contacts also have names, `email`, `phone`, your `userId`, an avatar, notes and tags. Their communication fields include channel `subscriptions`, `topicIds`, source and email statistics. Objects have `externalId`, notes and tags. Add fields such as status, price or renewal date for your own model. Business events have `eventId`, `occurredAt`, `receivedAt`, `readyAt`, `readinessState` and `definitionRevision`, plus an optional explicit `contactId` or `objectId` subject. See [Events](/events#add-the-fields-it-carries) for the difference between acceptance and readiness. Platform-maintained fields are read-only even when they appear alongside writable fields. Published calculated values also have their own read-only context and [expansion](/attribute-expansion) rules; do not import a `traits` column as customer data. ## How maxclicks decides a record is one you already have Matching happens inside the selected schema. For contacts, a platform ID identifies a record directly. Otherwise the supplied `userId` takes priority; without it, email is used, and without either, phone is used. This is not a search across every identifier until any match is found. A new `userId` can describe a different profile from an email-only record. Objects use the platform ID or an `externalId` within the object schema. Without a usable identity, a write can create another record instead of matching the one you intended. Conflicting identifiers are handled differently by different write operations. Review the [contact identity rules](/contacts#how-maxclicks-knows-two-records-are-the-same-person), [object identity rules](/objects#give-every-object-an-external-id) and the response from your chosen API operation. Event retry detection is separate and uses `eventId` with an unchanged occurrence payload. ## Name it something maxclicks can use Schema names and slugs must be available within the workspace. The form checks availability and normalizes names; names are 2–200 characters and cannot use reserved data-type terms such as `string`, `number`, `object` or `event`. Use a business name such as Customer, Subscription or PurchaseCompleted. Choose slugs for longevity because integrations and API calls use them. Use display names and descriptions to improve clarity later without changing those references. ## Your default contact schema The first contact schema becomes the workspace default. A **Default** badge identifies it; use **Set as default** on another contact schema to change the default. The default helps operations that do not specify a schema. It does not move existing contacts, merge schemas or override an explicit selection. Keep at least one contact schema in the workspace. ## Change a schema later The **Configuration** tab lets you change names and the description. The type and slug remain fixed. Integration-owned schemas are locked against manual configuration changes; their own integration maintains those definitions. Deleting a schema permanently removes the records it owns. Dependencies and the last-contact-schema requirement can block deletion. Read the reported references and update the consuming definitions before retrying. Deletion is an asynchronous operation when cleanup is required. The app waits for its status; the initial accepted request is not the same as completed cleanup. Other workspace processing can be temporarily gated while deletion removes related data. Do not use deletion as a way to rename a populated schema. Changing a populated field or an event collection contract can require [setup review](/setup-changes). Inspect proposed values, saved consumers and completion before relying on the new definition. ## Related * [Attributes](/attributes): stored fields, evaluated relationships and scheduled measures. * [Contacts](/contacts) and [Objects](/objects): identity, editing and imports. * [Events](/events): business occurrences and typed collection sources. * [Segments](/segments): apply the model to an audience. * [Schemas API](/api/schemas) and [Records API](/api/records): inspect definitions and write supported records. --- # SDKs Source: https://docs.maxclicks.ai/sdks # SDKs Use an official maxclicks SDK to connect customer data, send events, and trigger prepared campaigns from your application. Choose from eight languages, install the package, and authenticate with your API key. Each guide covers installation, client configuration, requests, pagination, errors, and retries. For a walkthrough of the underlying requests and responses, follow the [HTTP quickstart](/quickstart). Configure schemas, brand, content, and journeys in the app. Your integration keeps customer data current, triggers the experience, and reads the results. Client libraries follow the same endpoint permissions and limits as direct API requests. ## Current API coverage The SDKs use Public API v1. Follow your language guide for method names and options, and the [API reference](/api) for request fields, response states, and endpoint behavior. | Capability | Integration guidance | | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | | Event acceptance and readiness | Save the acceptance receipt and [check readiness](/api/events#events.status) before relying on downstream processing. | | Event identity changes | Follow the [identity contract](/api/events#eventIdentities.identify) and preserve operation IDs across retries. | | Record deletion | Save the deletion operation ID and [poll its status](/api/records#records.getDeletion) until completion. | | Event batch results | Inspect each result, including rolled-back operations and acceptance receipts. | | Workflow history | Continue through history pages using the cursor returned by [the run](/api/workflows#workflows.getRun). | | Idempotent writes | Send a unique key for each logical operation on [supported endpoints](/api/idempotency#supported-endpoints), and reuse it for retries. | The [CLI](/ai/cli) and [MCP server](/ai/mcp) provide terminal and assistant interfaces. Every API reference endpoint also includes direct HTTP examples. ## Install Install the package for your application's language. Follow the linked guide for dependency configuration and a first request. | Language | Package | Install | Runtime | | ---------------------- | -------------------------------------- | ------------------------------------------------------ | ------------ | | [Go](/sdks/go) | `github.com/maxclicks-ai/maxclicks-go` | `go get github.com/maxclicks-ai/maxclicks-go` | Go 1.22+ | | [Node.js](/sdks/node) | `maxclicks` | `npm install maxclicks` | Node.js 18+ | | [Python](/sdks/python) | `maxclicks` | `pip install maxclicks` | Python 3.9+ | | [PHP](/sdks/php) | `maxclicks/maxclicks-php` | `composer require maxclicks/maxclicks-php` | PHP 8.1+ | | [Ruby](/sdks/ruby) | `maxclicks` | `gem install maxclicks` | Ruby 2.7+ | | [Java](/sdks/java) | `ai.maxclicks:maxclicks-java` | [Add a Maven or Gradle dependency](/sdks/java#install) | Java 17+ | | [Rust](/sdks/rust) | `maxclicks` | `cargo add maxclicks` | Rust 1.70+ | | [Elixir](/sdks/elixir) | `maxclicks` | [Add the Hex dependency](/sdks/elixir#install) | Elixir 1.15+ | The Python and Ruby SDKs have zero runtime dependencies. Go and Rust use the standard library and `reqwest` respectively. Java depends on Jackson. PHP uses any PSR-18 client, discovered at runtime. ## Quickstart Start with `GET /v1/me` through your chosen client. Confirm the key and space before writing customer data or sending email. Each language guide includes its own initialization and request examples. Store `MAXCLICKS_API_KEY` on your server. The default base URL is `https://api.maxclicks.ai/v1`; do not embed the key in client-side code. When you are ready to write, use the [API quickstart](/quickstart) to understand the schema and template requirements, then follow the equivalent methods in your SDK guide. ## Shared contract The API contract stays the same across languages. Constructor options, method signatures, and pagination helpers follow each language's conventions; consult the language guide for exact usage. `Authentication` (Bearer) The API key is sent as `Authorization: Bearer max_...`. It falls back to the `MAXCLICKS_API_KEY` environment variable. The public form and email endpoints (`forms.submit`, `forms.confirmDoubleOptIn`, one-click unsubscribe) send no auth header. `Envelope unwrap` ({ data }) The API returns a `{ "data": ... }` envelope. SDKs return the unwrapped payload directly. Wire keys stay verbatim (camelCase, for example `firstName`, `namePlural`), because records carry arbitrary custom attribute keys. `Pagination` (auto) List methods expose pagination information. Use the language-specific page or iterator helpers to traverse results, and respect the offset cap of 10,000. Events use cursor pagination. See [pagination](/api/pagination). `Errors` (typed) Failures surface as typed errors carrying `status`, `code`, `type`, `message`, response `headers`, and the raw body. See [errors](/api/errors). `Retries` (2 (default)) SDKs provide bounded backoff for transient failures. Writes require special care: pass an idempotency key only to a supported endpoint, preserve it across retries, and handle unresolved outcomes. A retry may replay a recorded error; it does not guarantee another execution. See your language guide for its retry options and the [idempotency contract](/api/idempotency). `Idempotency` (Idempotency-Key) Pass an idempotency key on [supported writes](/api/idempotency#supported-endpoints) using the option documented in your language guide. Use a fresh key for each logical operation and preserve it across retries. Reusing one constant can cause unrelated calls to replay the first result. See [idempotency](/api/idempotency). `Timeout` (60s (default)) Per-request timeout, configurable at construction. `Warnings` (callback) The API returns non-fatal warnings (for example a clamped `limit`). Pass an `onWarning` callback to observe them. ## Errors Each SDK maps HTTP status to a typed error under one base class (`MaxclicksError` in Node, `Maxclicks::Error` in Ruby, `MaxclicksException` in PHP and Java, `Maxclicks.Error` in Elixir, `MaxclicksError` in Rust, `*APIError` in Go). | Status | Meaning | | ------------ | ------------------------------------------- | | `400` | Validation failure | | `401` | Missing or invalid API key | | `402` | Insufficient credits or email limit reached | | `403` | Insufficient permission | | `404` | Resource not found | | `409` | Identifier or uniqueness conflict | | `413`, `415` | Payload too large, unsupported media type | | `422` | Unprocessable (for example a batch abort) | | `429` | Rate limited (carries retry-after) | | `5xx` | Server error | Transport failures, timeouts, and missing configuration raise their own error types before or instead of an HTTP response. Validation-gate failures (such as `broadcasts.send`) carry an `issues` list with each problem. All SDKs speak Public API v1. Method and argument names follow each language's idiom (camelCase in Node, snake\_case in Python and Ruby), but resource names, endpoint behavior, and error taxonomy come from the REST contract. ## Related * [Authentication](/api/authentication) * [Pagination](/api/pagination) * [Idempotency](/api/idempotency) * [Errors](/api/errors) --- # Elixir SDK Source: https://docs.maxclicks.ai/sdks/elixir # Elixir SDK Connect your Elixir application to the customer experiences your team builds in maxclicks. The client wraps the Public API with resource modules, tagged results, and structured errors. Functions return `{:ok, value}` or `{:error, %Maxclicks.Error{}}`. Each has a `!` sibling that returns the value or raises. ## Current API coverage The SDK groups Public API calls by resource. Use the [API reference](/api) for request fields, response payloads, and operation semantics, and the examples below for language-specific method calls. Pagination helpers follow the [API traversal limits](/api/pagination). An iterator or method named `all` stops at the traversal cap; use scoped queries when working with larger datasets. ## Install Add the SDK to the dependencies in `mix.exs`: ```elixir defp deps do [ {:maxclicks, "~> 1.1"} ] end ``` Run `mix deps.get` to install it. The SDK starts its own Finch pool (`Maxclicks.Finch`), so you add nothing to your supervision tree. ## Client Build a client with `Maxclicks.new/1`. The API key falls back to the `MAXCLICKS_API_KEY` environment variable when `:api_key` is not given. ```elixir client = Maxclicks.new(api_key: "max_...") # Identify the calling key {:ok, me} = Maxclicks.me(client) # Upsert a contact by identity; input is a plain map of attribute values {:ok, contact} = Maxclicks.Records.upsert(client, "students", %{ "email" => "ada@example.com", "firstName" => "Ada", "loyaltyTier" => "gold" }) # Custom attributes are preserved and readable Maxclicks.Model.Record.get_string(contact, "loyaltyTier") # Send a template {:ok, result} = Maxclicks.Templates.send(client, template_id, %{"contact" => %{"email" => "ada@example.com"}}) ``` ### Options `api_key` (string) Your API key. Defaults to `MAXCLICKS_API_KEY`. Base URL is `https://api.maxclicks.ai/v1`. `base_url` (string) Override the base URL. Use `Maxclicks.Client.stage_base_url()` for staging. `timeout` (integer) Per-request timeout in milliseconds. `max_retries` (integer) Extra attempts on `429` and `5xx` for reads and for writes that pass `:idempotency_key`. A keyless write is never replayed, since it may already have landed. Retries use full-jitter backoff and honor `Retry-After`. `default_headers` (map) Headers sent with every request. `on_warning` (function) Callback `fn warnings, %{method: method, path: path} -> ... end` invoked on non-fatal API warnings. `transport` (tuple) Swappable HTTP transport implementing the `Maxclicks.Transport` behaviour. Defaults to Finch. ## Resources Each API group is a module on the client. `Maxclicks.Schemas`, `Maxclicks.Attributes`, `Maxclicks.Records`, `Maxclicks.Suppressions`, `Maxclicks.Events`, `Maxclicks.Domains`, `Maxclicks.Senders`, `Maxclicks.Topics`, `Maxclicks.Segments`, `Maxclicks.Broadcasts`, `Maxclicks.Templates`, `Maxclicks.Webhooks`, `Maxclicks.Workflows`, `Maxclicks.Forms`, `Maxclicks.Emails`, and `Maxclicks.me/1`. ## Errors `Maxclicks.Error` carries a `type` plus `status`, `code`, `message`, `headers`, `raw`, and `retry_after`. Match on `type` to branch. ```elixir case Maxclicks.Records.get(client, "students", id) do {:ok, record} -> record {:error, %Maxclicks.Error{type: :not_found}} -> nil {:error, error} -> raise error end # Or use the raising variant record = Maxclicks.Records.get!(client, "students", id) ``` | `type` | Meaning | | ----------------------- | --------------------------------- | | `:bad_request` | `400` malformed request. | | `:authentication` | `401` missing or invalid API key. | | `:permission` | `403` key lacks permission. | | `:not_found` | `404` resource does not exist. | | `:conflict` | `409` identity or state conflict. | | `:unprocessable_entity` | `422` validation failure. | | `:rate_limit` | `429`; `retry_after` is set. | | `:server` | `5xx` server error. | | `:api` | API returned an unexpected shape. | | `:connection` | Transport could not connect. | | `:timeout` | Request exceeded `timeout`. | | `:configuration` | Client is misconfigured. | ## Pagination List endpoints return a `Maxclicks.Page`, or a `Maxclicks.CursorPage` for events. ```elixir {:ok, page} = Maxclicks.Records.list(client, "students", limit: 100) page.data page.pagination.total_count case Maxclicks.Page.next_page(page) do {:ok, next} -> next :end -> :done end ``` Iterate every item lazily with `stream/*`, which pages automatically. ```elixir client |> Maxclicks.Records.stream("students") |> Stream.filter(& &1.email) |> Enum.take(500) ``` ## Warnings The API surfaces non-fatal warnings, such as a clamped `limit`. Read them from `page.warnings`, or pass an `:on_warning` callback to observe every warning. ## Testing without a network The `Maxclicks.Transport` behaviour is the seam. Implement it in your own test support module and the SDK never touches the network. The example below shows a transport you can add to your application tests. ```elixir defmodule MyApp.StubTransport do @behaviour Maxclicks.Transport @impl true def request(_request, _opts) do {:ok, %{ status: 200, headers: %{"content-type" => "application/json"}, body: ~s({"data": {}}), final_url: "https://api.maxclicks.ai/v1/resource" }} end end client = Maxclicks.new( api_key: "max_test", transport: MyApp.StubTransport, sleep_fun: fn _ -> :ok end ) ``` The SDK is MIT licensed. Contact management maps to [records](/contacts); template sends map to [templates](/templates). ## Retry outcomes A repeated write can return a recorded result, including an error. If the API reports `idempotency_outcome_unknown`, inspect the affected resource or run history before creating a new operation. Keep the original key and payload while you reconcile. See the [idempotency guide](/api/idempotency) for supported operations and retention. --- # Go SDK Source: https://docs.maxclicks.ai/sdks/go # Go SDK Build a maxclicks integration into your Go service. Sync customer data, fire events, and trigger prepared experiences with context-aware requests and typed results. ## Current API coverage The SDK groups Public API calls by resource. Use the [API reference](/api) for request fields, response payloads, and operation semantics, and the examples below for language-specific method calls. Pagination helpers follow the [API traversal limits](/api/pagination). An iterator or method named `all` stops at the traversal cap; use scoped queries when working with larger datasets. ## Install ```sh go get github.com/maxclicks-ai/maxclicks-go ``` ## Quickstart The client reads `MAXCLICKS_API_KEY` from the environment, or pass a key with `WithAPIKey`. The key is not validated at construction: a missing key surfaces as `*maxclicks.ConfigurationError` on the first authenticated call. ```go package main import ( "context" "fmt" "log" "github.com/maxclicks-ai/maxclicks-go" ) func main() { client := maxclicks.New(maxclicks.WithAPIKey("max_...")) ctx := context.Background() me, err := client.Me(ctx) if err != nil { log.Fatal(err) } fmt.Println("space:", me.Space.Slug) contact, err := client.Records.Upsert(ctx, "students", maxclicks.RecordInput{ "email": "ada@example.com", "firstName": "Ada", "tags": []string{"vip"}, }) if err != nil { log.Fatal(err) } fmt.Println("contact id:", contact.ID) } ``` ## Configuration `maxclicks.New` accepts functional options. | Option | Default | Description | | ------------------------------------------------------------- | ----------------------------- | ----------------------------------------------------- | | `WithAPIKey(string)` | `MAXCLICKS_API_KEY` env var | Bearer API key. | | `WithBaseURL(string)` | `https://api.maxclicks.ai/v1` | API base URL. Use `maxclicks.StageBaseURL` for stage. | | `WithTimeout(time.Duration)` | `60s` | Per-request timeout. | | `WithMaxRetries(int)` | `2` | Extra retry attempts on retryable failures. | | `WithHTTPClient(*http.Client)` | `&http.Client{}` | Inject a custom client or transport. | | `WithDefaultHeader(k, v string)` | none | Add a header to every request. | | `WithDefaultHeaders(map[string]string)` | none | Add multiple headers. | | `WithOnWarning(func(warnings []string, method, path string))` | none | Callback for API warnings. | ## Errors Every failure is a typed error. HTTP error responses map by status. Use `errors.As` against a specific type, or `maxclicks.AsAPIError` against the base `*maxclicks.APIError`. ```go _, err := client.Schemas.Get(ctx, "does-not-exist") var notFound *maxclicks.NotFoundError if errors.As(err, ¬Found) { fmt.Println("missing:", notFound.Message, notFound.Code) } if apiErr, ok := maxclicks.AsAPIError(err); ok { fmt.Println(apiErr.Status, apiErr.Type, apiErr.Code, apiErr.Message) } var rateLimit *maxclicks.RateLimitError if errors.As(err, &rateLimit) { time.Sleep(rateLimit.RetryAfter) } ``` | Status or condition | Error type | | ------------------- | ------------------------------------ | | 400 | `*BadRequestError` | | 401 | `*AuthenticationError` | | 402 | `*PaymentRequiredError` | | 403 | `*PermissionError` | | 404 | `*NotFoundError` | | 405 | `*MethodNotAllowedError` | | 409 | `*ConflictError` | | 413 | `*PayloadTooLargeError` | | 415 | `*UnsupportedMediaTypeError` | | 422 | `*UnprocessableEntityError` | | 429 | `*RateLimitError` (has `RetryAfter`) | | 5xx | `*ServerError` | | transport failure | `*ConnectionError` | | timeout or deadline | `*TimeoutError` | | missing API key | `*ConfigurationError` | Every HTTP error type embeds `*APIError`, which carries `Status`, `Code`, `Type`, `Message`, `Issues`, `Header`, and `RawBody`. `Issues` is populated for validation-gate failures (such as `broadcasts.send`'s `broadcast_invalid`) that report several problems at once. `402` signals `insufficient_credits` (an AI-generation or auto-fill call) or `email_limit_reached` (a send past the email allowance). Retries are limited to what is safe to repeat. Reads retry on `429`, any `5xx`, and transport or timeout errors, up to `WithMaxRetries` extra attempts. A write retries on those only when you pass `WithIdempotencyKey`, since the server then collapses the repeat into the original; a keyless write that times out or hits a `5xx` is reported to you instead of being sent again, so your code can inspect the outcome. The client also retries `429` responses according to its configured retry budget. Backoff is exponential with full jitter, and a `Retry-After` header (seconds or HTTP date) sets the delay floor. ## Pagination List endpoints return a `*maxclicks.Page[T]` and a matching `...All` iterator that walks every page (stopping when `hasMore` is false or the maximum offset is reached). ```go // Single page: page, err := client.Records.List(ctx, "students", &maxclicks.ListOptions{Limit: 100}) for _, r := range page.Data { fmt.Println(r.ID) } if page.HasNextPage() { page, err = page.NextPage(ctx) } // Auto-pager over every record: it := client.Records.ListAll(ctx, "students", nil) for it.Next(ctx) { fmt.Println(it.Value().ID) } if err := it.Err(); err != nil { log.Fatal(err) } ``` `client.Events.List` is cursor-paginated, not offset-paginated. It returns a `*maxclicks.CursorPage[Event]` and a matching `ListAll` iterator, reading ready events within the selected time window. `HasNextPage`, `NextPage`, and `Iterator` work the same way. Records and events preserve unknown custom-attribute keys in an `Extra map[string]any` field alongside the typed base fields. ## Idempotency Mutating endpoints the API documents as idempotency-aware accept `maxclicks.WithIdempotencyKey(...)` as a trailing option. A retry with the same key replays the original result instead of executing twice. ```go key := uuid.NewString() contact, err := client.Records.Upsert(ctx, "students", maxclicks.RecordInput{"email": "ada@example.com"}, maxclicks.WithIdempotencyKey(key), ) ``` Pass an idempotency key to writes such as `Records.Create`, `Records.Upsert`, `Suppressions.BatchCreate`, `Suppressions.BatchDelete`, `Templates.Send`, `Broadcasts.Send`, and `Workflows.Trigger`. See the [supported API writes](/api/idempotency#supported-endpoints) and retry contract before replaying an operation. ## Warnings The API returns non-fatal warnings in the response body (`warnings` array), or, for the empty-body `Workflows.Trigger` response, in the `maxclicks-Warning-Message` header. Register a handler to observe them. ```go client := maxclicks.New( maxclicks.WithAPIKey("max_..."), maxclicks.WithOnWarning(func(warnings []string, method, path string) { log.Printf("warning on %s %s: %v", method, path, warnings) }), ) ``` `*Page[T]` also exposes the warnings for that page as `page.Warnings`. ## Resource surface Methods are grouped by resource on the client. Each list method also exposes a matching `...All` auto-paging iterator. `client.Me` (GET /me) The authenticated space and API key. `client.Schemas` (/schemas) `List`, `Get`. `client.Attributes` (/schemas/{schema}/attributes) `List`. `client.Records` (/schemas/{schema}/records) `List`, `Create`, `Upsert`, `Get`, `Update`, `Delete`, `AuditTrail`. `client.Suppressions` (/contacts/suppressions) `List`, `Create`, `BatchCreate`, `BatchDelete`, `Delete`. Admin key. `client.Events` (/events) `List` (cursor), `Fire`, `FireBatch`. `client.Domains` (/domains) `List`, `Get`. `client.Senders` (/senders) `List`. `client.Topics` (/topics) `List`, `Get`. `client.Segments` (/segments) `List`, `Create`, `Get`, `Delete`, `Count`, `ListContacts`. `client.Broadcasts` (/broadcasts) `List`, `Get`, `Update`, `Send`, `ListRuns`, `GetMetrics`. `client.Templates` (/templates) `List`, `Get`, `Send`. `client.Webhooks` (/webhooks) `List`, `Create`, `Get`, `Update`, `Delete`, `RotateSecret`. `client.Workflows` (/workflows) `List`, `Get`, `Trigger`, `Pause`, `Unpause`, `ListRuns`, `GetRun`. `client.Forms` (/forms/{formId}) `Submit`, `ConfirmDoubleOptIn`. No auth. `client.Emails` (/emails/{emailId}) `UnsubscribeOneClick`. No auth. Notable behaviors: * `Events.FireBatch` returns a `*BatchFireResult` even when the API responds `422` (a batch with failed items is a business result, not an error). Pass `maxclicks.BatchOnErrorContinue` or `maxclicks.BatchOnErrorAbort`. * `Templates.Send` is synchronous and returns a `*TemplateSendResponse`. A genuine send failure comes back as `Status: "failed"` with a generic `Error`. Pre-send validation and eligibility failures are typed errors. * `Workflows.Trigger` returns an empty body. Its path segment is a workflow-step reference, not a workflow id. * `Forms.ConfirmDoubleOptIn` and `Emails.UnsubscribeOneClick` follow the browser redirect and return a `*RedirectResult` with the final landing URL. Create schemas, attributes, domains, senders, topics, templates and broadcasts in the maxclicks app, then use resource methods to read them or act on prepared work. Manage API keys and CSV imports in the app. See the [API reference](/api) for request fields and response details. ## Rate limits Per API key: 100 read requests per second and 25 write requests per second. Exceeding a limit returns `429`. The SDK retries automatically, honoring `Retry-After`. The module path is `github.com/maxclicks-ai/maxclicks-go`. The SDK is MIT licensed. ## Retry outcomes A repeated write can return a recorded result, including an error. If the API reports `idempotency_outcome_unknown`, inspect the affected resource or run history before creating a new operation. Keep the original key and payload while you reconcile. See the [idempotency guide](/api/idempotency) for supported operations and retention. --- # Java SDK Source: https://docs.maxclicks.ai/sdks/java # Java SDK Connect your Java service to customer data, events, and prepared campaigns in maxclicks. Use resource methods to call the Public API and typed exceptions to handle failures. The Java package is `ai.maxclicks`. The current version is `1.1.0`. ## Current API coverage The SDK groups Public API calls by resource. Use the [API reference](/api) for request fields, response payloads, and operation semantics, and the examples below for language-specific method calls. Pagination helpers follow the [API traversal limits](/api/pagination). An iterator or method named `all` stops at the traversal cap; use scoped queries when working with larger datasets. ## Install Add the SDK dependency with Maven or Gradle: ```xml {{ title: 'Maven' }} ai.maxclicks maxclicks-java 1.1.0 ``` ```groovy {{ title: 'Gradle' }} repositories { mavenCentral() } dependencies { implementation 'ai.maxclicks:maxclicks-java:1.1.0' } ``` ## Client Build a client with `Maxclicks.builder()`. Pass `apiKey`, or set the `MAXCLICKS_API_KEY` environment variable and call `.build()` with no key. ```java import ai.maxclicks.Maxclicks; import ai.maxclicks.models.Me; import ai.maxclicks.models.MaxclicksRecord; import java.util.Map; Maxclicks mc = Maxclicks.builder() .apiKey("max_...") .build(); Me me = mc.me(); System.out.println(me.space().slug()); // Records are flat maps with any custom attribute keys. MaxclicksRecord contact = mc.records().upsert("students", Map.of( "email", "ada@example.com", "firstName", "Ada", "loyaltyTier", "gold")); System.out.println(contact.id() + " " + contact.getString("loyaltyTier")); ``` Every request carries `Authorization: Bearer `, `Accept: application/json`, and `X-Maxclicks-Client: maxclicks-java/`. The public form and email endpoints (`forms().submit`, `forms().confirmDoubleOptIn`, `emails().unsubscribeOneClick`) send no auth header. ## Configuration | Option | Default | Description | | --------------- | ----------------------------- | ----------------------------------------------------- | | `apiKey` | `MAXCLICKS_API_KEY` env | Bearer API key. | | `baseUrl` | `https://api.maxclicks.ai/v1` | Override with `Maxclicks.STAGE_BASE_URL` for staging. | | `timeout` | 60s | Per-request timeout (`Duration`). | | `maxRetries` | 2 | Additional attempts on retryable failures. | | `transport` | JDK `HttpClient` | Injectable `Transport` for testing. | | `defaultHeader` | none | Extra header merged into every request. | | `onWarning` | none | Callback for API warnings. | ```java Maxclicks mc = Maxclicks.builder() .apiKey(System.getenv("MAXCLICKS_API_KEY")) .baseUrl(Maxclicks.STAGE_BASE_URL) .timeout(java.time.Duration.ofSeconds(30)) .maxRetries(4) .onWarning((warnings, ctx) -> System.err.println(ctx.path() + ": " + warnings)) .build(); ``` ## Resources The client mirrors the v1 API across sixteen groups. Management resources return typed models and take builder-style params. Contact, object, and event data is passed and returned as flat `Map`. | Accessor | Covers | | --------------------------- | ------------------------------------------------------------ | | `mc.me()` | `GET /me` | | `mc.schemas()` | list and get schemas | | `mc.attributes()` | list the stored and evaluated attributes on a schema | | `mc.records()` | create, upsert, list, get, update, delete, audit trail | | `mc.suppressions()` | list, create, batch create, batch delete, delete (admin key) | | `mc.events()` | fire, fire batch, list (cursor-paginated) | | `mc.domains()` | list and get domains | | `mc.senders()` | list sender profiles | | `mc.topics()` | list and get communication topics | | `mc.segments()` | list, create, get, delete, count, contacts | | `mc.broadcasts()` | list, get, update, send, runs, metrics | | `mc.templates()` | list, get, send | | `mc.webhooks()` | webhooks and secret rotation | | `mc.workflows()` | list, get, trigger, pause, unpause, runs, get run | | `mc.forms()`, `mc.emails()` | public form submit and one-click unsubscribe | Several groups read but do not create. Schemas, attributes, domains, senders and topics are authored in the maxclicks app, as are templates and broadcasts (which Java can still send, and a broadcast it can still update). Supported structural field changes use [setup review](/setup-changes), which checks existing values and saved consumers before applying a conversion. A template or broadcast needs its content and recipient preview checked before sending. There is no public record-batch endpoint; use individual writes and their documented identity, rate-limit and auto-fill rules. Configure the entity in the app, then drive it from Java with the send, trigger and read methods below. ```java mc.records().create(schema, input); // POST .../records mc.records().upsert(schema, input); // POST .../records/upsert mc.records().get(schema, id); // GET .../records/{id} mc.templates().send(templateId, data); // POST /templates/{id}/send mc.events().fire(schema, input); // POST /events/{schema} mc.workflows().trigger(reference, body); // POST /workflows/{reference} ``` `MaxclicksRecord` and `Event` preserve every field. Use `id()`, `email()`, and other accessors for known fields, and `get(key)`, `getString(key)`, or `fields()` for custom attributes. An explicit `null` in an input map clears a nullable field. ## Pagination List methods return a `Page` that is `Iterable` and auto-fetches subsequent pages as you iterate. Iteration stops when `hasMore` is false or the offset would exceed 10,000. ```java import ai.maxclicks.Page; import ai.maxclicks.models.MaxclicksRecord; Page page = mc.records().list("students"); page.data(); // this page only page.pagination(); // limit / offset / totalCount / hasMore for (MaxclicksRecord r : mc.records().list("students")) { // all pages System.out.println(r.email()); } var all = mc.records().list("students").all(); // materialize everything var emails = mc.records().list("students").stream() // lazy over all pages .map(MaxclicksRecord::email) .toList(); ``` `limit` (1..200, default 50) and `offset` (0..10000, default 0) are trailing arguments on every offset-paginated list method. `events().list` is the exception: it is cursor-paginated and takes no `offset`. `events().list(...)` reads ready events from the operational store using cursor pagination. It returns a `CursorPage` with the same iteration methods (`data()`, `iterator()`, `all()`, `stream()`), driven by an opaque `nextCursor` instead of an offset. The default window is 7 days. ## Errors Every failure throws an unchecked subclass of `MaxclicksException`. API errors carry `statusCode()`, `code()`, `type()`, `headers()`, and `raw()`. ```java import ai.maxclicks.errors.*; try { mc.schemas().get("does-not-exist"); } catch (MaxclicksNotFoundException e) { System.out.println(e.statusCode() + " " + e.code() + " " + e.getMessage()); } catch (MaxclicksRateLimitException e) { System.out.println("retry after ms: " + e.retryAfterMs()); } catch (MaxclicksApiException e) { // any other 4xx / 5xx } catch (MaxclicksException e) { // configuration, connection, or timeout error } ``` | Exception | When | | --------------------------------------- | ------------------------------------------- | | `MaxclicksBadRequestException` | 400 | | `MaxclicksAuthenticationException` | 401 | | `MaxclicksPermissionException` | 403 | | `MaxclicksNotFoundException` | 404 | | `MaxclicksConflictException` | 409 | | `MaxclicksUnprocessableEntityException` | 422 | | `MaxclicksRateLimitException` | 429 (carries `retryAfterMs()`) | | `MaxclicksServerException` | 5xx | | `MaxclicksConnectionException` | transport failure (DNS, TCP, TLS) | | `MaxclicksTimeoutException` | request exceeded the timeout | | `MaxclicksConfigurationException` | programmer error, such as a missing API key | Retries are limited to what is safe to repeat. Reads are retried on `429`, `5xx`, and transport or timeout errors, up to `maxRetries` times. A write is retried on those only when it carries an `Idempotency-Key`, which is the trailing `idempotencyKey` argument on `records().create`, `records().upsert`, `suppressions().batchCreate`, `suppressions().batchDelete`, `templates().send`, `broadcasts().send`, and `workflows().trigger`. A keyless write that times out is reported to you rather than sent twice. The client also retries `429` responses according to its configured retry budget. Backoff is exponential with full jitter (base \~500ms, cap \~8s), and a `Retry-After` header sets the delay floor. `events().fireBatch` treats a `422` as a normal result parsed into `BatchFireResult`, not a thrown error. `templates().send` returns a `TemplateSendResponse` whose `status` is `"failed"` on a genuine send failure, while pre-send validation errors throw. ## Retry outcomes A repeated write can return a recorded result, including an error. If the API reports `idempotency_outcome_unknown`, inspect the affected resource or run history before creating a new operation. Keep the original key and payload while you reconcile. See the [idempotency guide](/api/idempotency) for supported operations and retention. --- # Node.js SDK Source: https://docs.maxclicks.ai/sdks/node # Node.js SDK Connect customer records, events, and prepared campaigns from your Node.js or TypeScript service. The client exposes the Public API through resource methods, typed errors, and pagination helpers. ## Current API coverage The SDK groups Public API calls by resource. Use the [API reference](/api) for request fields, response payloads, and operation semantics, and the examples below for language-specific method calls. Pagination helpers follow the [API traversal limits](/api/pagination). An iterator or method named `all` stops at the traversal cap; use scoped queries when working with larger datasets. ## Install Install the SDK with npm: ```bash npm install maxclicks ``` ## Authenticate Pass an [API key](/api/authentication) to `new Maxclicks(...)`, or set `MAXCLICKS_API_KEY` and call `new Maxclicks()`. ```ts import { Maxclicks } from 'maxclicks' const mc = new Maxclicks('max_...') const me = await mc.me() console.log(me.space?.name) ``` CommonJS works too: ```js const { Maxclicks } = require('maxclicks') ``` ### Configuration `apiKey` (string) Your API key. Defaults to the `MAXCLICKS_API_KEY` environment variable. `baseUrl` (string) API base URL. Defaults to `https://api.maxclicks.ai/v1`. `timeoutMs` (number) Per-request timeout. Defaults to `60000`. `maxRetries` (number) Extra attempts on `429`, plus `5xx` and transport errors for reads and calls that carry an idempotency key. Defaults to `2`. `onWarning` (function) Callback for non-fatal API warnings. ```ts const mc = new Maxclicks({ apiKey: process.env.MAXCLICKS_API_KEY, baseUrl: 'https://api.maxclicks.ai/v1', timeoutMs: 60000, maxRetries: 2, onWarning: (warnings) => console.warn(warnings), }) ``` ## Send and upsert `schema` is a schema id or slug. Records are flat: base fields plus your custom [attribute](/attributes) values at the top level. ```ts {{ title: 'Node' }} // Upsert a contact by identity (id, userId, email, or phone). const contact = await mc.records.upsert('students', { email: 'ada@example.com', firstName: 'Ada', tags: ['beta'], }) // Send a template to a contact resolved by email. await mc.templates.send(templateId, { data: { contact: { email: 'ada@example.com', firstName: 'Ada' } }, }) // Fire an event. await mc.events.fire('purchase-completed', { eventId: 'ord_123', amount: 42 }) ``` ```bash {{ title: 'cURL' }} curl -X POST https://api.maxclicks.ai/v1/schemas/students/records/upsert \ -H "Authorization: Bearer max_..." \ -H "Content-Type: application/json" \ -d '{ "email": "ada@example.com", "firstName": "Ada", "tags": ["beta"] }' ``` `records.create` is a strict create and returns `409` if the identity exists. `records.upsert` creates or updates by identity. See [Contacts](/contacts) for the identity cascade. ## Follow asynchronous work Acceptance is not completion. An event's receipt carries its `id`, which `events.status` reads for readiness. A record deletion returns an operation that you poll until it completes. ```ts const receipt = await mc.events.fire('purchase-completed', { eventId: 'ord_123', amount: 42, }) const { readinessState } = await mc.events.status( 'purchase-completed', receipt.id, ) const deletion = await mc.records.delete('students', contact.id) const { status } = await mc.records.getDeletion(deletion.operationId) ``` `readinessState` is `pending`, `ready`, `failed` or `outcome_unknown`. A deletion is `pending`, `blocked`, `completed` or `cancelled`; while it reports `canCancel`, the user who requested it can stop it with `records.cancelDeletion`. `templates.send` and `records.delete` always send an `Idempotency-Key`, generating one when you pass none. To retry later without sending or deleting twice, pass your own `{ idempotencyKey }` and reuse it. Older workflow run history pages with `workflows.getRun(id, runId, { historyBefore })`. ## Pagination Every offset-paginated `list` returns a `Page`. A `Page` is both a single page and an async iterable over every item across all pages. It auto-fetches subsequent pages as you iterate. `events.list` is the exception: it is cursor-paginated and returns a plain `EventPage` with `data`, `pagination` and `warnings`, which you page by passing `pagination.nextCursor` back as `cursor`. ```ts // One page. const page = await mc.records.list('students', { limit: 100 }) console.log(page.data, page.pagination.totalCount, page.pagination.hasMore) // Every record, auto-fetching subsequent pages. for await (const record of await mc.records.list('students')) { console.log(record.id) } // Or collect them all. const all = await (await mc.records.list('students')).all() ``` ## Errors Every failure throws a typed subclass of `MaxclicksError`. The happy path returns data directly. ```ts import { MaxclicksError, MaxclicksNotFoundError, MaxclicksRateLimitError, } from 'maxclicks' try { const record = await mc.records.get('students', 'missing-id') } catch (error) { if (error instanceof MaxclicksNotFoundError) { // 404 } else if (error instanceof MaxclicksRateLimitError) { console.log('retry after', error.retryAfterMs) } else if (error instanceof MaxclicksError) { console.error(error.status, error.code, error.type, error.message) } } ``` | Error | When | | ---------------------------------------------------- | ------------------------------------------------- | | `MaxclicksBadRequestError` | 400 validation failure | | `MaxclicksAuthenticationError` | 401 missing or invalid API key | | `MaxclicksPermissionError` | 403 insufficient permission or space out of scope | | `MaxclicksNotFoundError` | 404 resource not found | | `MaxclicksConflictError` | 409 identifier or uniqueness conflict | | `MaxclicksUnprocessableEntityError` | 422 (for example a batch abort) | | `MaxclicksRateLimitError` | 429 rate limited (carries `retryAfterMs`) | | `MaxclicksServerError` | 5xx server error | | `MaxclicksConnectionError` / `MaxclicksTimeoutError` | transport failure or timeout | | `MaxclicksConfigurationError` | client misconfiguration (for example no API key) | The SDK retries reads on `429`, `5xx`, and transport errors with jittered exponential backoff, honoring the `Retry-After` header. A write is only retried on `5xx` and transport errors when it carries an idempotency key, one you pass or the one `templates.send` and `records.delete` generate, so your code can inspect an uncertain outcome. Every request is still retried on `429`. Tune the attempt count with `maxRetries`. ## Retry outcomes A repeated write can return a recorded result, including an error. If the API reports `idempotency_outcome_unknown`, inspect the affected resource or run history before creating a new operation. Keep the original key and payload while you reconcile. See the [idempotency guide](/api/idempotency) for supported operations and retention. ## Related * [Records and identity](/contacts) * [Authentication](/api/authentication) * [Pagination](/api/pagination) --- # PHP SDK Source: https://docs.maxclicks.ai/sdks/php # PHP SDK Connect maxclicks to your PHP application: update customer records, send events, trigger prepared emails, and read results through the Public API. ## Current API coverage The SDK groups Public API calls by resource. Use the [API reference](/api) for request fields, response payloads, and operation semantics, and the examples below for language-specific method calls. Pagination helpers follow the [API traversal limits](/api/pagination). An iterator or method named `all` stops at the traversal cap; use scoped queries when working with larger datasets. ## Install Install the SDK with Composer: ```bash composer require maxclicks/maxclicks-php ``` The SDK auto-discovers any installed PSR-18 client and PSR-17 factories via `php-http/discovery`. If you have none, install one: ```bash composer require guzzlehttp/guzzle nyholm/psr7 ``` ## Client Pass an API key, or call `new Maxclicks()` to read `MAXCLICKS_API_KEY`. The default base URL is `https://api.maxclicks.ai/v1`. ```php use Maxclicks\Maxclicks; $mc = new Maxclicks('max_...'); $me = $mc->me(); echo $me->user['email']; ``` Resource groups are properties on the client (`$mc->records`, `$mc->events`, `$mc->templates`, and so on), plus a top-level `me()` method. ```php {{ title: 'Upsert a contact' }} $contact = $mc->records->upsert('students', [ 'email' => 'ada@example.com', 'firstName' => 'Ada', 'favoriteColor' => 'blue', // custom attribute ]); echo $contact->id(); echo $contact['favoriteColor']; // unknown keys preserved ``` ```php {{ title: 'Fire an event' }} $mc->events->fire('purchase_completed', [ 'eventId' => 'order_123', 'occurredAt' => '2025-01-01T12:00:00Z', 'amount' => 49.0, ]); ``` ```php {{ title: 'Send a template' }} // Ids are opaque strings from templates->list() or the app, opaque; preserve the complete value, including any prefix. $mc->templates->send('cmb3k1x2p0000s601abcdefgh', [ 'contact' => ['email' => 'ada@example.com'], ]); ``` `Record` and `Event` models are flat and keep the full payload. Read base fields with typed accessors (`id()`, `email()`, `tags()`) and any key with array access, `get('key')`, or `toArray()`. Template, broadcast, segment and webhook ids are opaque strings the API returns; do not assume a uuid or a fixed prefix. Schemas take either a slug (`'students'`) or an id, and `workflows->trigger()` takes the reference id of the workflow's incoming-webhook step. ## Resource surface The live v1 surface is `schemas`, `attributes`, `records`, `suppressions`, `events`, `domains`, `senders`, `topics`, `segments`, `broadcasts`, `templates`, `webhooks`, `workflows`, `forms` and `emails`, plus `me()` on the client. Create schemas, attributes, domains, senders, topics, templates and broadcasts in the maxclicks app, then use resource methods to read them or act on prepared work. Manage API keys and CSV imports in the app. See the [API reference](/api) for request fields and response details. Build the entity once in the app, then drive it from PHP: `$mc->broadcasts->send()`, `$mc->templates->send()`, `$mc->workflows->trigger()`, and the whole read surface. ## Configuration Pass a bare API key, an options array, or a `Maxclicks\Config`. | Option | Default | Notes | | ---------------- | ----------------------------- | --------------------------------------------------------------------------------- | | `apiKey` | `MAXCLICKS_API_KEY` env | Bearer token, sent on all but public endpoints. | | `baseUrl` | `https://api.maxclicks.ai/v1` | Use `Config::STAGE_BASE_URL` for staging. | | `timeout` | `60.0` seconds | Honored by clients that support it. | | `maxRetries` | `2` | Retries on 429, plus 5xx and transport/timeout errors for reads and keyed writes. | | `defaultHeaders` | `[]` | Merged into every request. | | `onWarning` | `null` | `fn(array $warnings, string $method, string $path)`. | ```php use Maxclicks\Config; use Maxclicks\Maxclicks; $mc = new Maxclicks([ 'apiKey' => 'max_...', 'baseUrl' => Config::STAGE_BASE_URL, 'timeout' => 60.0, 'maxRetries' => 2, // Inject your own PSR-18 client / PSR-17 factories: // 'httpClient' => $psr18Client, // 'requestFactory' => $psr17RequestFactory, // 'streamFactory' => $psr17StreamFactory, ]); ``` ## Pagination List methods return a `Maxclicks\Pagination\Page`. It is both the current page and a lazy iterator over every page. ```php $page = $mc->records->list('students', limit: 100); foreach ($page->data as $record) { /* this page */ } $page->pagination->totalCount; foreach ($mc->records->list('students') as $record) { echo $record->id(); // fetches next pages on demand } $all = $mc->records->list('students')->all(); ``` Auto-pagination stops when `hasMore` is false or the offset would exceed 10000. `events->list()` uses cursor pagination and returns a `Maxclicks\Pagination\CursorPage`, which follows `nextCursor` automatically. ## Idempotency The writes that support it accept an optional trailing `idempotencyKey`. Replaying a key returns the original result instead of acting twice. ```php $mc->records->create( 'students', ['email' => 'ada@example.com', 'firstName' => 'Ada'], idempotencyKey: 'signup-2026-07-18-ada', ); ``` Pass an idempotency key to writes such as `records->create`, `records->upsert`, `suppressions->batchCreate`, `suppressions->batchDelete`, `templates->send`, `broadcasts->send`, and `workflows->trigger`. See the [supported API writes](/api/idempotency#supported-endpoints) and retry contract before replaying an operation. Event fires are deliberately not key-aware: dedupe them with your own `eventId`. See [Idempotency](/api/idempotency) for the wire-level contract. ## Errors Every failure throws a typed exception extending `Maxclicks\Exceptions\MaxclicksException`. API errors carry `status`, `code`, `type`, `issues`, response `headers`, and the `raw` body. ```php use Maxclicks\Exceptions\MaxclicksApiException; use Maxclicks\Exceptions\MaxclicksNotFoundException; use Maxclicks\Exceptions\MaxclicksRateLimitException; try { $mc->records->get('students', 'missing-id'); } catch (MaxclicksNotFoundException $e) { // 404 } catch (MaxclicksRateLimitException $e) { sleep((int) ceil($e->retryAfterSeconds ?? 1.0)); } catch (MaxclicksApiException $e) { error_log("{$e->status} {$e->code}: {$e->getMessage()}"); } ``` | Exception | When | | --------------------------------------- | ---------------------------------- | | `MaxclicksBadRequestException` | 400 | | `MaxclicksAuthenticationException` | 401 | | `MaxclicksPermissionException` | 403 | | `MaxclicksNotFoundException` | 404 | | `MaxclicksConflictException` | 409 | | `MaxclicksUnprocessableEntityException` | 422 | | `MaxclicksRateLimitException` | 429 (has `retryAfterSeconds`) | | `MaxclicksServerException` | 5xx | | `MaxclicksConnectionException` | transport failure (DNS, TCP, TLS) | | `MaxclicksTimeoutException` | request timed out | | `MaxclicksConfigurationException` | programmer error (missing API key) | Retries are limited to what is safe to repeat. Reads retry on `429`, `5xx`, and transport or timeout errors. A write retries on those only when it carries an `idempotencyKey`, so a keyless send that times out is thrown to you rather than sent again. The client also retries `429` responses according to its configured retry budget. Backoff is exponential with full jitter (base 0.5s, cap 8s), and a `Retry-After` header on a 429 sets the delay floor. Some 422s are validation gates, not batch results. Sending a not-ready broadcast throws `MaxclicksUnprocessableEntityException` with `$e->issues` listing the problems. ```php use Maxclicks\Exceptions\MaxclicksUnprocessableEntityException; try { $mc->broadcasts->send('cmb3k1x2p0000s601wxyz9876'); } catch (MaxclicksUnprocessableEntityException $e) { foreach ($e->issues ?? [] as $issue) { error_log($issue); } } ``` `events->fireBatch()` is not an exception case. It returns a `BatchFireResult` even when the API returns 422 for partial failure. Check `hasFailures()` and iterate `results`. ## Warnings Successful responses may include warnings, in the body's `warnings` array or in the `maxclicks-Warning-Message` response header. Provide an `onWarning` callback to receive them, which is the only way to see warnings from methods like `templates->send()` and `workflows->trigger()` that return nothing to the caller. Paginated results also expose `$page->warnings`. ## Laravel The package ships a service provider and a `Maxclicks` facade, auto-discovered by Laravel. Set your key in the environment: ```dotenv MAXCLICKS_API_KEY=max_... # optional: MAXCLICKS_BASE_URL=https://api.maxclicks.ai/v1 MAXCLICKS_TIMEOUT=60 MAXCLICKS_MAX_RETRIES=2 ``` Publish the config file if you want to edit it: ```bash php artisan vendor:publish --tag=maxclicks-config ``` The facade forwards top-level calls (like `me()`) to the shared client. Resource groups are properties, so reach them through the resolved instance. ```php use Maxclicks\Laravel\Maxclicks; $me = Maxclicks::me(); $contact = app(\Maxclicks\Maxclicks::class) ->records->upsert('students', ['email' => 'ada@example.com']); // or inject the client public function __construct(private \Maxclicks\Maxclicks $maxclicks) {} ``` The client also falls back to `config/services.php` under the `maxclicks` key (`['maxclicks' => ['key' => env('MAXCLICKS_API_KEY')]]`). ## Rate limits The Public API allows about 100 read requests per second and 25 write requests per second per key. The SDK retries `429` responses automatically, honoring `Retry-After`. ## Retry outcomes A repeated write can return a recorded result, including an error. If the API reports `idempotency_outcome_unknown`, inspect the affected resource or run history before creating a new operation. Keep the original key and payload while you reconcile. See the [idempotency guide](/api/idempotency) for supported operations and retention. --- # Python SDK Source: https://docs.maxclicks.ai/sdks/python # Python SDK Keep customer data and events in sync from your Python service. The client provides resource methods, structured errors, and pagination helpers for the maxclicks Public API. * Zero required runtime dependencies (pure standard library `urllib`). * Fully type-hinted, ships a `py.typed` marker. * Python 3.9+. ## Current API coverage The SDK groups Public API calls by resource. Use the [API reference](/api) for request fields, response payloads, and operation semantics, and the examples below for language-specific method calls. Pagination helpers follow the [API traversal limits](/api/pagination). An iterator or method named `all` stops at the traversal cap; use scoped queries when working with larger datasets. ## Install Install the SDK with pip: ```bash pip install maxclicks ``` ## Initialize Pass your API key, or set `MAXCLICKS_API_KEY` and construct with no arguments. ```python from maxclicks import Maxclicks client = Maxclicks("max_...") # or set MAXCLICKS_API_KEY me = client.me() print(me["space"]) ``` `client.me()` identifies the calling key, its owner, the bound space, and role. ## Configuration Construct the client with any of these options. | Option | Default | Notes | | ----------------- | ----------------------------- | -------------------------------------------------------------------------------------------- | | `api_key` | `MAXCLICKS_API_KEY` env | Bearer key. Required for all but public endpoints. | | `base_url` | `https://api.maxclicks.ai/v1` | Use `maxclicks.STAGE_BASE_URL` for staging. | | `timeout` | `60.0` | Per-request timeout in seconds. | | `max_retries` | `2` | Extra attempts on `429`, plus `5xx` and transport/timeout errors for reads and keyed writes. | | `default_headers` | `{}` | Merged into every request. | | `on_warning` | `None` | `(warnings: list[str], ctx)` callback. | ## Response shape Responses are the unwrapped payload of the API's `{ "data": ... }` envelope, returned as plain `dict` and `list` values. Wire keys are preserved verbatim in camelCase, for example `record["firstName"]` and `schema["namePlural"]`, because record and event payloads carry user-defined custom-attribute keys. SDK method names and keyword arguments use snake\_case. ## Example Upsert a contact, iterate every record, fire an event, and send a template. ```python {{ title: 'Records' }} contact = client.records.upsert("students", {"email": "ada@example.com", "firstName": "Ada"}) print(contact["id"]) for record in client.records.list("students"): print(record["id"], record.get("email")) ``` ```python {{ title: 'Events' }} client.events.fire("purchase_completed", {"eventId": "evt_123", "amount": 49.0}) ``` ```python {{ title: 'Templates' }} # Ids are opaque strings from templates.list or the app, opaque; preserve the complete value, including any prefix. client.templates.send( "cmb3k1x2p0000s601abcdefgh", data={"contact": {"email": "ada@example.com"}}, ) ``` `records.upsert` creates or updates by identity (contact: `id`/`userId`/`email`/`phone`; object: `id`/`externalId`). `records.create` never updates an existing record and returns `409` on conflict. Template, broadcast, segment and webhook ids are opaque strings the API returns; do not assume a uuid or a fixed prefix. Schemas take either a slug (`"students"`) or an id, and `workflows.trigger` takes the reference id of the workflow's incoming-webhook step. ## Pagination List methods return a `Page` that holds the first page and iterates lazily across every following page. Iteration stops when `hasMore` is false or the offset cap of 10,000 is reached. ```python page = client.records.list("students", limit=100) page.data # this page's items page.pagination # {"limit", "offset", "totalCount", "hasMore"} page.warnings # warnings for this page for record in page: # every item across all pages, auto-fetching ... everything = client.records.list("students").all() # collect into a list ``` `events.list` is cursor-paginated and returns a `CursorPage` with the same interface. Its `pagination` is `{"limit", "nextCursor", "hasMore"}`. ```python for event in client.events.list("purchase_completed", limit=200): print(event["id"]) ``` ## Errors Every failure raises a typed exception. All API errors carry `status`, `code`, `type`, `message`, `headers`, and the raw `body`. | Exception | Status | | ----------------------------------- | ------------------------------------------- | | `MaxclicksBadRequestError` | 400 | | `MaxclicksAuthenticationError` | 401 | | `MaxclicksPermissionError` | 403 | | `MaxclicksNotFoundError` | 404 | | `MaxclicksConflictError` | 409 | | `MaxclicksUnprocessableEntityError` | 422 | | `MaxclicksRateLimitError` | 429 (has `.retry_after_seconds`) | | `MaxclicksServerError` | 5xx | | `MaxclicksConnectionError` | transport failure | | `MaxclicksTimeoutError` | timeout | | `MaxclicksConfigurationError` | programmer error, raised before any request | `MaxclicksError` is the base class for all of them. ```python from maxclicks import MaxclicksNotFoundError, MaxclicksRateLimitError, MaxclicksError try: client.records.get("students", "missing-id") except MaxclicksNotFoundError as error: print(error.status, error.code, error.message) except MaxclicksRateLimitError as error: print("retry after", error.retry_after_seconds, "seconds") except MaxclicksError as error: print("something went wrong:", error) ``` Reads are retried on `429`, any `5xx`, and transport/timeout errors, up to `max_retries` extra attempts, using exponential backoff with full jitter (base \~0.5s, cap \~8s). A write is retried on those only when you pass `idempotency_key`; without one, a timed-out write is reported to you rather than repeated, because it may already have landed. The client also retries `429` responses according to its configured retry budget. A `Retry-After` header sets the delay floor. ## Idempotency Write endpoints that support it accept an `idempotency_key`. Retrying with the same key returns the original result instead of acting twice. ```python client.records.upsert( "students", {"email": "ada@example.com", "firstName": "Ada"}, idempotency_key="signup-2026-07-18-ada", ) ``` Pass an idempotency key to writes such as `records.create`, `records.upsert`, `suppressions.batch_create`, `suppressions.batch_delete`, `templates.send`, `broadcasts.send`, and `workflows.trigger`. See the [supported API writes](/api/idempotency#supported-endpoints) and retry contract before replaying an operation. ## Warnings The API may attach warnings to a response, in the body's `warnings` array or, for empty-body endpoints like `workflows.trigger`, in the URL-encoded `maxclicks-Warning-Message` response header. Provide `on_warning` to receive them. ```python client = Maxclicks(on_warning=lambda warnings, ctx: print(ctx.method, ctx.path, warnings)) ``` ## Rate limits Per API key: 100 requests/second for reads and 25 requests/second for writes. Exceeding a limit returns `429`, which the SDK retries with the server's `Retry-After` delay. ## Resource surface API resources are reachable as `me`, `schemas`, `attributes`, `records`, `suppressions`, `events`, `domains`, `senders`, `topics`, `segments`, `broadcasts`, `templates`, `webhooks`, `workflows`, `forms`, and `emails`. | Resource | Methods | | --------------------- | -------------------------------------------------------------- | | `client.me()` | Current key context (a method on the client itself) | | `client.schemas` | `list` `get` | | `client.attributes` | `list` | | `client.records` | `create` `upsert` `list` `get` `update` `delete` `audit_trail` | | `client.suppressions` | `list` `create` `batch_create` `batch_delete` `delete` | | `client.events` | `fire` `fire_batch` `list` | | `client.domains` | `list` `get` | | `client.senders` | `list` | | `client.topics` | `list` `get` | | `client.segments` | `list` `create` `get` `delete` `count` `list_contacts` | | `client.broadcasts` | `list` `get` `update` `send` `list_runs` `get_metrics` | | `client.templates` | `list` `get` `send` | | `client.webhooks` | `create` `list` `get` `update` `delete` `rotate_secret` | | `client.workflows` | `list` `get` `trigger` `pause` `unpause` `list_runs` `get_run` | | `client.forms` | `submit` `confirm_double_opt_in` | | `client.emails` | `unsubscribe_one_click` | Create schemas, attributes, domains, senders, topics, templates and broadcasts in the maxclicks app, then use resource methods to read them or act on prepared work. Manage API keys and CSV imports in the app. See the [API reference](/api) for request fields and response details. Set the entity up once in the app, then trigger, read and measure it from Python: build a broadcast in the app and call `broadcasts.send`, design a template in the app and call `templates.send`, publish a workflow in the app and call `workflows.trigger`. The SDK is MIT-licensed and open source. ## Retry outcomes A repeated write can return a recorded result, including an error. If the API reports `idempotency_outcome_unknown`, inspect the affected resource or run history before creating a new operation. Keep the original key and payload while you reconcile. See the [idempotency guide](/api/idempotency) for supported operations and retention. --- # Ruby SDK Source: https://docs.maxclicks.ai/sdks/ruby # Ruby SDK Connect your Ruby application to customer records, events, and prepared campaigns in maxclicks. The client groups API operations by resource and provides structured errors and pagination. ## Current API coverage The SDK groups Public API calls by resource. Use the [API reference](/api) for request fields, response payloads, and operation semantics, and the examples below for language-specific method calls. Pagination helpers follow the [API traversal limits](/api/pagination). An iterator or method named `all` stops at the traversal cap; use scoped queries when working with larger datasets. ## Install Add the gem to your `Gemfile`, then run `bundle install`. ```ruby gem "maxclicks", "~> 1.0" ``` ## Client Create a `Maxclicks::Client` with an API key. The key falls back to `ENV["MAXCLICKS_API_KEY"]`. ```ruby require "maxclicks" client = Maxclicks::Client.new(api_key: "max_...") # Upsert a contact. Custom attribute keys are allowed alongside built-in fields. contact = client.records.upsert("students", { "email" => "ada@example.com", "firstName" => "Ada", "loyaltyPoints" => 42, }) puts contact.id puts contact["loyaltyPoints"] # custom keys via [] # Fire an event. client.events.fire("purchase", { "eventId" => "ord_1001", "amount" => 79.0 }) # Send a stored template. Ids are opaque strings from templates.list or the app. client.templates.send_template("cmb3k1x2p0000s601abcdefgh", data: { "contact" => { "email" => "ada@example.com" } }) ``` ### Options `api_key` (string) Bearer key. Defaults to `ENV["MAXCLICKS_API_KEY"]`. Required for every endpoint except the public form and email endpoints. `base_url` (string) Defaults to `https://api.maxclicks.ai/v1`. Stage is `https://api-stage.maxclicks.ai/v1`. `timeout` (integer) Per-request timeout in seconds. Default 60. `max_retries` (integer) Extra attempts on retryable failures. Default 2. `default_headers` (hash) Headers merged into every request. `on_warning` (proc) Callback `->(warnings, context)` invoked when the API returns non-fatal warnings. `transport` (object) Any object responding to `call(Maxclicks::HTTP::Request) -> Maxclicks::HTTP::Response`. The seam for testing without a network or plugging in a different HTTP stack. The public endpoints `forms.submit`, `forms.confirm_double_opt_in`, and `emails.unsubscribe_one_click` send no `Authorization` header. ## Resources Endpoints are grouped by resource on the client. `send_template` and `send_broadcast` carry a suffix so they do not shadow Ruby's `Object#send`; `broadcasts.send` is kept as an alias. | Resource | Methods | | --------------------- | -------------------------------------------------------------- | | `client.me` | Current key context | | `client.schemas` | `list` `get` | | `client.attributes` | `list` | | `client.records` | `create` `upsert` `list` `get` `update` `delete` `audit_trail` | | `client.suppressions` | `list` `create` `batch_create` `batch_delete` `delete` | | `client.events` | `list` `fire` `fire_batch` | | `client.domains` | `list` `get` | | `client.senders` | `list` | | `client.topics` | `list` `get` | | `client.segments` | `list` `create` `get` `delete` `count` `list_contacts` | | `client.broadcasts` | `list` `get` `update` `send_broadcast` `list_runs` `metrics` | | `client.templates` | `list` `get` `send_template` | | `client.webhooks` | `create` `list` `get` `update` `delete` `rotate_secret` | | `client.workflows` | `list` `get` `trigger` `pause` `unpause` `list_runs` `get_run` | | `client.forms` | `submit` `confirm_double_opt_in` | | `client.emails` | `unsubscribe_one_click` | Create schemas, attributes, domains, senders, topics, templates and broadcasts in the maxclicks app, then use resource methods to read them or act on prepared work. Manage API keys and CSV imports in the app. See the [API reference](/api) for request fields and response details. ## Pagination List endpoints return a `Maxclicks::Page`. Use `.data` for the current page, or any `Enumerable` method to walk every page automatically. Auto-pagination stops at `has_more == false` or the offset cap of 10,000. ```ruby page = client.records.list("students", limit: 100) page.data # this page's records page.pagination # # page.warnings # Walk every page: client.records.list("students").each { |record| puts record.id } all = client.records.list("students").to_a first_ten = client.records.list("students").first(10) # Manual page walking: page = page.next_page if page.next_page? ``` `events.list` is cursor-paginated and returns a `Maxclicks::CursorPage`. It exposes the same interface but advances by an opaque `next_cursor` with no total count or offset cap. ```ruby client.events.list(schema: "purchase", from: "2026-07-01T00:00:00Z").each do |event| puts event.id end page = client.events.list(schema: "purchase", limit: 100) page.pagination.next_cursor # opaque cursor, or nil on the last page ``` ## Errors Every failure raises a subclass of `Maxclicks::Error`. Each error carries `status`, `code` (nullable), `type`, `headers`, `raw` (the parsed body), and `issues` (validation problems on multi-problem gates, else `nil`). ```ruby begin client.records.get("students", "missing") rescue Maxclicks::NotFoundError => e puts e.status # 404 puts e.code # e.g. "record_not_found" rescue Maxclicks::RateLimitError => e sleep(e.retry_after || 1) # seconds parsed from Retry-After rescue Maxclicks::Error => e # any SDK failure end ``` | Class | When | | -------------------------------------- | ------------------------------------------------------------------ | | `Maxclicks::ConfigurationError` | Programmer error (e.g. missing API key), raised before any request | | `Maxclicks::BadRequestError` | 400 | | `Maxclicks::AuthenticationError` | 401 | | `Maxclicks::PaymentRequiredError` | 402 (`insufficient_credits`, `email_limit_reached`) | | `Maxclicks::PermissionError` | 403 | | `Maxclicks::NotFoundError` | 404 | | `Maxclicks::ConflictError` | 409 | | `Maxclicks::PayloadTooLargeError` | 413 | | `Maxclicks::UnsupportedMediaTypeError` | 415 | | `Maxclicks::UnprocessableEntityError` | 422 | | `Maxclicks::RateLimitError` | 429 (carries `retry_after`) | | `Maxclicks::ServerError` | 5xx | | `Maxclicks::ConnectionError` | Transport failure (DNS, TCP, TLS, reset) | | `Maxclicks::TimeoutError` | Request exceeded the timeout (subclass of `ConnectionError`) | The SDK retries reads on `429`, any `5xx`, and transport or timeout errors, up to `max_retries` extra attempts with exponential backoff and full jitter (base \~0.5s, cap \~8s). A write is retried on those only when it carries an `idempotency_key:`; without one, a timed-out write is raised to you rather than repeated, because it may already have landed. The client retries every call on `429` within its retry budget. Check the error code: event receipt or identity capacity failures can need reconciliation instead of more automatic retries. A `Retry-After` header sets the delay floor. The API enforces roughly 100 read requests per second and 25 write requests per second per key. AI-backed writes (`segments.create` and `webhooks.create`/`webhooks.update` with a custom-filter condition) are limited to 10 requests per minute. ## Idempotency Mutating writes accept an optional `idempotency_key:`. Pass a unique string and the API de-duplicates retried requests: a replay returns the original result. ```ruby client.records.upsert("students", { "email" => "ada@example.com", "firstName" => "Ada" }, idempotency_key: "signup-2026-07-18-ada") ``` Pass an idempotency key to writes such as `records.create`, `records.upsert`, `suppressions.batch_create`, `suppressions.batch_delete`, `templates.send_template`, `broadcasts.send_broadcast`, and `workflows.trigger`. See the [supported API writes](/api/idempotency#supported-endpoints) and retry contract before replaying an operation. ## Endpoint notes * `events.fire_batch`: a `422` is a normal partial-failure result, returned as a `Maxclicks::BatchFireResult` with `results`, `summary`, and (on `on_error: "abort"`) `failed_index`. It is not raised. * `broadcasts.send_broadcast`: a `422 broadcast_invalid` is raised as `Maxclicks::UnprocessableEntityError`, with `issues` listing what is missing. `send` is an alias. * `templates.send_template` and `workflows.trigger`: return `nil`. Warnings arrive via the `on_warning` callback. * `forms.confirm_double_opt_in` and `emails.unsubscribe_one_click`: return a `Maxclicks::RedirectResult` with `status` and `redirect_url`. ## Type signatures Full [RBS](https://github.com/ruby/rbs) signatures ship in `sig/` and are packaged with the gem, so `steep` and `rbs` can type-check code that uses the SDK. Validate them with `rbs -I sig validate`. ## Retry outcomes A repeated write can return a recorded result, including an error. If the API reports `idempotency_outcome_unknown`, inspect the affected resource or run history before creating a new operation. Keep the original key and payload while you reconcile. See the [idempotency guide](/api/idempotency) for supported operations and retention. --- # Rust SDK Source: https://docs.maxclicks.ai/sdks/rust # Rust SDK Connect maxclicks to your Rust service with explicit request results and errors. The client covers customer data, events, prepared campaigns, and workflow triggers. Minimum supported Rust version is 1.70. ## Current API coverage The SDK groups Public API calls by resource. Use the [API reference](/api) for request fields, response payloads, and operation semantics, and the examples below for language-specific method calls. Pagination helpers follow the [API traversal limits](/api/pagination). An iterator or method named `all` stops at the traversal cap; use scoped queries when working with larger datasets. ## Install Add the SDK to your `Cargo.toml`: ```toml [dependencies] maxclicks = "0.2.0" ``` ## Quickstart Construct a `Client` with your API key, then call resource groups. Records and event inputs are flat and accept any `Serialize` value, including `serde_json::json!`. ```rust use maxclicks::Client; use serde_json::json; fn main() -> Result<(), maxclicks::MaxclicksError> { let client = Client::new("max_...")?; // Identify the calling key. let me = client.me()?; println!("space: {:?}", me.space); // Upsert a contact by identity. Arbitrary custom keys are preserved. let contact = client.records().upsert( "students", &json!({ "email": "ada@example.com", "firstName": "Ada", "plan": "pro" }), None, // optional Idempotency-Key )?; println!("contact id: {}", contact.id); // Walk every record across all pages. for record in client.records().list_all("students") { let record = record?; println!("{}", record.id); } Ok(()) } ``` ## Client Construct a client three ways. | Constructor | Behavior | | ------------------------ | ---------------------------------------- | | `Client::new("max_...")` | Bare API key. | | `Client::from_env()` | Reads `MAXCLICKS_API_KEY`. | | `Client::builder()` | Full control over transport and headers. | ```rust use maxclicks::Client; use std::time::Duration; let client = Client::builder() .api_key("max_...") .base_url("https://api.maxclicks.ai/v1") .timeout(Duration::from_secs(30)) .max_retries(3) .default_header("X-My-Trace", "abc") .on_warning(|warnings, context| { eprintln!("{} {}: {:?}", context.method, context.path, warnings); }) .build()?; ``` `api_key` (string) Sent as `Authorization: Bearer `. Falls back to `MAXCLICKS_API_KEY`. `base_url` (string) Defaults to `https://api.maxclicks.ai/v1`. Trailing slashes are trimmed. `timeout` (Duration) Per request. Default 60s. `max_retries` (u32) Extra attempts on retryable failures. Default 2. `on_warning` (callback) Invoked with API warnings per call. `transport` (Arc\) Defaults to blocking `reqwest`. Inject a mock to test without a network. ## Errors Every call returns `Result`. Match a variant, or use the uniform accessors `error.status()`, `error.code()`, and `error.message()`. ```rust use maxclicks::MaxclicksError; match client.schemas().get("missing") { Ok(schema) => println!("{}", schema.name), Err(MaxclicksError::NotFound(data)) => eprintln!("no such schema: {}", data.message), Err(MaxclicksError::RateLimit { retry_after, .. }) => { eprintln!("slow down; retry after {:?}", retry_after); } Err(error) => eprintln!( "status={:?} code={:?} message={}", error.status(), error.code(), error.message(), ), } ``` | Variant | Status | Notes | | --------------------- | ------ | ----------------------------------------------------------------------------------- | | `BadRequest` | 400 | | | `Authentication` | 401 | | | `PaymentRequired` | 402 | `insufficient_credits` on an AI or auto-fill call, `email_limit_reached` on a send. | | `Permission` | 403 | | | `NotFound` | 404 | | | `Conflict` | 409 | | | `UnprocessableEntity` | 422 | | | `RateLimit` | 429 | Carries `retry_after`. | | `Server` | 5xx | | | `Api` | other | 405/413/415 or an undecodable body. | | `Connection` | | Transport failure. | | `Timeout` | | Request timed out. | | `Config` | | Programmer error such as a missing key, raised before any request. | ### Retries and rate limits Reads are retried on `429`, any `5xx`, and transport or timeout errors, up to `max_retries` extra attempts. A write is retried on those only when it carries an idempotency key; without one it is retried on `429` alone, because a `5xx` or a timeout leaves the outcome unknown and a repeat could send twice. Backoff is exponential with full jitter, base 500ms, cap 8s. A `Retry-After` header (delay-seconds or HTTP date) sets the delay floor. Per-key limits are 100 req/s for reads and 25 req/s for writes. ## Pagination List endpoints return a `Page` with `data`, `pagination`, and `warnings`. Every list method except `workflows().runs` has a `*_all(...)` variant returning a `Paginator` that walks every page. It stops when `has_more` is false or at the offset cap of 10,000. Page `workflows().runs` yourself with its `limit` and `offset` arguments. `events().list_all(...)` is the cursor-paginated exception. It returns a `CursorPaginator` driven by the opaque `next_cursor` rather than an offset, and takes the same `from` and `to` window as `events().list`. ```rust // One page. let page = client.records().list("students", Some(100), Some(0))?; println!("{} of {}", page.data.len(), page.pagination.total_count); // Every item, auto-fetching pages. for record in client.records().list_all("students") { let record = record?; // ... } // Or collect all at once. Short-circuits on the first error. let all = client.records().list_all("students").collect_all()?; ``` ## Idempotency Writes such as `records().create`, `records().upsert`, `suppressions().create_batch`, `suppressions().remove_batch`, `templates().send`, `broadcasts().send`, and `workflows().trigger` take a trailing `Option<&str>` [Idempotency-Key](/api/idempotency). Pass `None` to skip. See the [supported API writes](/api/idempotency#supported-endpoints) and retry contract before replaying an operation. ```rust client.records().upsert( "students", &json!({ "email": "ada@example.com", "firstName": "Ada" }), Some("signup-2026-07-18-ada"), )?; ``` ## Warnings The API can attach warnings in the response body under `warnings`. For the empty-`data` `workflows.trigger` response, warnings arrive in the URL-encoded `maxclicks-Warning-Message` response header. Both surface on `Page.warnings` and through the `on_warning` callback. ## Resource surface Each group is a method on `Client`. Every list method also has an auto-paginating `*_all(...)` variant, with the single exception of `workflows().runs`. | Group | Methods | | ---------------- | -------------------------------------------------------------------- | | *root* | `me` | | `schemas()` | `list`, `get` | | `attributes()` | `list` | | `records()` | `create`, `upsert`, `list`, `get`, `update`, `delete`, `audit_trail` | | `suppressions()` | `create`, `create_batch`, `remove_batch`, `list`, `delete` | | `events()` | `fire`, `fire_batch`, `list` | | `domains()` | `list`, `get` | | `senders()` | `list` | | `topics()` | `list`, `get` | | `segments()` | `create`, `list`, `get`, `delete`, `count`, `contacts` | | `broadcasts()` | `list`, `get`, `update`, `send`, `runs`, `metrics` | | `templates()` | `list`, `get`, `send` | | `webhooks()` | `create`, `list`, `get`, `update`, `delete`, `rotate_secret` | | `workflows()` | `list`, `get`, `pause`, `unpause`, `runs`, `get_run`, `trigger` | | `forms()` | `submit`, `confirm_double_opt_in` | | `emails()` | `unsubscribe_one_click` | Create schemas, attributes, domains, senders, topics, templates and broadcasts in the maxclicks app, then use resource methods to read them or act on prepared work. Manage API keys and CSV imports in the app. See the [API reference](/api) for request fields and response details. `forms.submit`, `forms.confirm_double_opt_in`, and `emails.unsubscribe_one_click` are unauthenticated. AI-backed calls (`segments().create` and `webhooks()` writes carrying a custom-filter condition) return `PaymentRequired` when the space is out of credits. ## Retry outcomes A repeated write can return a recorded result, including an error. If the API reports `idempotency_outcome_unknown`, inspect the affected resource or run history before creating a new operation. Keep the original key and payload while you reconcile. See the [idempotency guide](/api/idempotency) for supported operations and retention. --- # Segments Source: https://docs.maxclicks.ai/segments # Segments A segment is a reusable definition of who qualifies for a customer decision. Describe the behavior, time window and exclusions; maxinja generates the condition, and you inspect both its explanation and the matching contacts before using it. A segment selects contacts from one schema. It can use related objects, ready business events, evaluated relationships and published customer measures as context. It is a live query when used, not a permanently stored list of members. ## Start with the customer decision For a repeat-purchase campaign, define the action and its evidence: > Customers with at least two completed Orders, joined by Order.customerId to Customer.userId, whose latest purchasedAt was at least 14 days ago. Exclude canceled orders and contacts without an email address. This requires those schemas and fields to exist. State what “completed,” “recent” or “active” means in your own data. For money, specify currency and whether refunds count. For dates, specify which timestamp and time window you intend. Matching a segment does not establish delivery eligibility. Channel subscriptions, topics, suppressions and email validity still matter when a message is sent. The segment count is not a promise of that many deliveries. ## Use object data to select people Imagine Maya and Daniel belong to the same Pro account. Maya is its admin; Daniel is a member. A segment for “admins of Pro accounts” should return Maya's contact, using the account relationship to check the plan and the person's role to check eligibility. It does not return Studio North as a recipient. For commerce, “customers with two completed orders” returns the shopper once even when several Order objects establish the match. Be explicit about whose orders count; counting all purchases at the same company would answer a different question. The [objects walkthrough](/objects#how-objects-connect-to-people) shows the stored identifiers and evaluated relationship behind these examples. A generated condition can only use relationships supported by your actual records. ## Create a segment Open **Segments → Create segment**. Give it a unique **Name**, an optional **Description** explaining its purpose, and a **Contact schema** when more than one exists. The contact-schema choice becomes fixed once the generated condition is attached. Describe the audience and generate the condition. The form shows: * A readable description of the generated filter. * **What you asked maxinja**, preserving the requirement used to generate it. * A count and share of contacts, plus a searchable, paginated preview. Review the result, refine it if needed, generate the refinement and choose **Create segment**. If you change your intended audience, make sure the new instruction has been generated; saving the name does not reinterpret an unsubmitted prompt. ## Describe it well Use actual fields and relationships. “High-value customers” leaves the measure undefined. “Customers with completed purchase amount above 500 EUR in the last 30 days” gives the generation a concrete rule to implement. Explain missing values. A scheduled purchase count with no matching history is empty rather than zero. If your audience means “no observed purchase,” ask how empty and zero should be treated rather than assuming they match the same comparison. When reusing a saved segment, name it and state the extra condition. maxclicks tracks the resulting dependency so a source segment cannot disappear unnoticed. A saved segment also makes a shared definition visible to the rest of your team. ## Read the summary, not the code The generated expression remains on maxclicks servers. The description, count and sample contacts are your review surface. Check a known inclusion and exclusion, not only the total. Open a contact and inspect the fields that explain why they qualify. Search narrows the displayed members; it does not rewrite the saved segment. Counts are refreshed with the first page of a new listing request and reused while paging. The table is not a frozen snapshot, so records changing between requests can affect subsequent pages. For a segment using a [scheduled evaluated attribute](/attributes#computed-attributes), check that attribute's published status and **Data through** timestamp. Refreshing the segment preview does not recalculate the attribute's event history. An older publication can remain visible, but an enabled calculation that is unavailable or expired blocks a new decision that requests it. A paused calculation with a publication can use that retained result. ## What it costs Generating or refining a condition uses AI credits. Running the saved filter is a data query, not another generation call. Changing only a label does not require generating a new condition. If generation fails, read the error and check the selected schema, available fields and credits. If counting fails, treat **Unable to count contacts** as an unresolved check; it does not mean zero contacts match. ## Change a segment later Open the segment, edit its name or description, or generate a refined audience condition. Review the count and sample, then **Apply**. A segment referenced by an attribute, another segment, a broadcast, a workflow step or a webhook cannot have its condition rewritten. Its name and description can still change. Read the reported references before changing a shared audience. Integration-owned segments are managed by their integration. When dependencies prevent a changed meaning, create a new segment and deliberately move the consuming campaign or workflow to it. A display-name change is not a migration of the audience rule. ## Where you use a segment * [Broadcasts](/broadcast): select a reusable audience for a send. * [Workflows](/workflows): narrow a record trigger or find-records operation. * [Webhooks](/webhooks): restrict contact-related delivery to matching records. A segment can change between previews and send preparation. A broadcast then seals its selected audience for that send; later changes to segment membership do not continuously add new recipients. See [Large audiences](/large-audience). Saving a segment alone does not start a journey every time someone enters it. Choose an appropriate published workflow trigger. In particular, a [Evaluated attribute threshold trigger](/workflows) reacts to transitions in published scheduled values with its own baseline rules. ## Delete a segment Use its row menu to delete it. References from audiences, attributes, workflows or webhooks can block deletion; the error identifies what needs attention. Integration-owned segments cannot be deleted manually. Deleting the segment definition does not delete the contacts it selected. ## Who can do what Creating, changing and deleting segments requires workspace write access. Workspace members can view the audience definition and inspect its contacts according to their access. ## Related * [Contacts](/contacts): identity, communication preferences and record inspection. * [Evaluated attributes](/evaluated-attributes): reusable relationships and on-demand calculations. * [Scheduled evaluated attributes](/attributes#computed-attributes): published measures, empty values and freshness. * [Segments API](/api/segments): the supported generation, counting and listing operations. --- # Setup changes Source: https://docs.maxclicks.ai/setup-changes # Setup changes A setup change checks a proposed data-definition change against existing values and the saved work that reads them. Use it when changing a stored field's type or allowed values, replacing evaluated logic, adding a backfill, deleting an attribute, or changing an event's collection contract. For example, your Customer schema stores `planTier` as free text. You want a controlled list with `free`, `pro` and `enterprise`. The change must handle existing values such as `professional`, and the audience that compares them. Review the proposed conversions and campaign effects together before making the new meaning current. A save can finish automatically after its checks pass. Changes that need a decision appear in **Changes**. A pending check or reviewed proposal does not prove the schema changed. Application, reopening the space and resuming automation are also separate outcomes. ## Find the change Start from the field or event you intend to edit. Contact and object attributes live under **Settings → Schemas → Attributes**. Event attributes and bindings live under **Settings → Events**, on the event's **Attributes** and **Collection** tabs. Saving a structural edit runs setup checks. If a decision is needed, the app opens the relevant review. Reopen it from the schema's **Changes** tab. The **Changes** link at the top of **Settings → Events** opens workspace-wide history. The history lists changes that need a decision, rather than every ordinary name or description edit. Use **Review a batch** from **Changes** when related schema and attribute edits need to be proposed together. Review the whole proposal's affected definitions, not each field in isolation. Reducing the scope can also resolve a review that exceeds its supported input or processing bounds. Workspace Read permission is required to inspect a change. Write permission is required to propose, review, apply or cancel one. Integration-managed schema and attribute definitions are maintained by their integration; manual editing is restricted. ## State the intended business meaning Before editing, name what the field represents and which system supplies it. Explain units, allowed categories and missing-value behavior. For money, include currency and whether refunds or tax count. For activity, include the event schema, subject, occurrence-time window and schedule. Then identify the work that relies on it: a saved audience, an email field, a workflow branch, a form or a webhook. Changing `professional` to `pro` preserves a category only if those uses still interpret it as the same plan. A count that happens to stay unchanged is not enough to establish that meaning. Keep stable identifiers separate from display labels. An attribute's key and the schema's type and slug stay fixed. A name change does not rename the property expected by imports, producer payloads or templates. See [Attributes](/attributes) and [Schemas](/schemas) for those contracts. ## Choose what happens to existing values Changing a stored field offers **Existing records** policies. The policy is part of the proposed change, not a later cleanup task. | Policy | Result | When it is useful | | ----------------------------- | ----------------------------------------------------------- | ------------------------------------------------------- | | **Keep existing values** | Preserve each value and check it against the proposed rules | Widen a bound or add an option without rewriting values | | **Fill empty values** | Fill missing or empty values with one chosen constant | Give old records a deliberate known fallback | | **Map specific values** | Replace exact values using explicit pairs | Consolidate `Pro` and `professional` into `pro` | | **Convert to the new format** | Normalize values against the proposed type and rules | Convert compatible numeric text into a number | | **Replace all values** | Replace the attribute on every existing record | Deliberately reset a field, including populated records | For a value map, choose **Values without a mapping**: **Keep unchanged**, **Convert to the new format**, or **Block this change**. An unchanged value still has to fit the new rules. An unmapped category should block the change when silently carrying it forward would misclassify customers. Review exact values, including case and emptiness. Mapping `Pro` does not explain what `professional` means. Replacing all values is broader than filling empty values; inspect the affected population before choosing it. When adding a stored field, **Fill existing empty records** supplies an explicit constant. The creation-time policy remains separate. A default or AI autofill governs future new records that omit the field; it does not automatically backfill old records or rerun on updates. ## Read the checks and business comparisons A review shows proposed definitions, invalid values, affected saved items, pending work and the availability implications. Open details for each reported consumer. The checks can compare audience membership, workflow branches, message output or evaluated values when that comparison is available. For an audience, inspect added and removed examples as well as before/after counts. If both counts are 250, the same 250 people may still not be selected. For a message, inspect the actual changed subject and body. For a workflow, inspect the branch and whether the check used current records or captured run input. Read the stated coverage. **Complete**, sampled and unavailable evidence answer different questions. A sample does not establish every recipient's output. A check that cannot assess an item is unresolved evidence, rather than proof the item is unchanged. Current-record agreement also does not guarantee that every future valid value has the meaning your team intends. Saved checks describe their captured input. If records, definitions or calculated publications change, the app can require fresh checks before saving. Use **Refresh checks**, **Check again**, or **Prepare new review** when offered, then read the new result. Do not treat a previous screenshot as the current review. ## Resolve the reported blockers | Reported issue | Next action | | ---------------------------------------------------- | --------------------------------------------------------------------------------------- | | Values do not fit the new rules | Correct the conversion or source values and check again | | A saved rule cannot accept the proposed value domain | Open the dependent item, prepare the intended replacement and review it with the change | | A form still uses a removed field | Remove or repair that field reference through the review | | Event producer input would break | Update the producer contract and complete the required source sample | | Active work uses the old definition | Inspect the run or campaign and follow its reported waiting or stopping choice | | An external result is unresolved | Reconcile that action before relying on the change | | Review processing failed | Read the error; use **Retry failed checks** when the current review supports retry | The dependency report identifies the specific item to inspect. A different display name does not repair a rule, and deleting a field does not make its remaining references safe. Avoid replacing an entire audience merely to bypass a blocker without checking the customer consequence. Generation can prepare a replacement rule from your requirements. It uses AI credits where a generation action is requested. Review its explanation, affected inputs and resulting behavior; generation alone does not apply the proposal or establish correctness. ## Test an event producer A changed event contract can alter what a website or backend is allowed to submit. **Check incoming events** shows the source, environment, expected payload, test status and next action. A passing source sample is required when the review says so. The sample must use the proposed fields and the appropriate source authentication. The review supplies a test command for your developer. **Sample accepted** establishes that one authenticated sample passed; it does not prove your deployed website, mobile app or queued server jobs all use that format. Inspect both event properties and context. A collection binding takes properties from the event's attributes; its optional JSON **Context attribute** receives context and is excluded from producer properties. With no context attribute, the producer sends empty context. Native input must also satisfy the subject scope of a collection-bound event. A producer sample cannot remove a blocker for an unsupported change to retained history. Existing occurrences cannot be backfilled with a newly required field. Keep that field optional, or create a new event type when the new contract requires it. Accepted occurrences remain historical facts. Coordinate the producer rollout and workflow meaning with the source owner, and account for retrying refused events during an application pause. See [Events](/events#collected-events). ## Account for active workflows A workflow that is already running can hold captured input, published calculations or remaining actions that depend on the current setup. The review identifies that work. Inspect **View run** to distinguish a waiting customer journey from an action whose external result is unresolved. Where offered, a waiting plan retains new entries in the selected workflows while existing work finishes. It does not stop all record writes. Retained entries are checked against the current workflow after application or cancellation; retaining an entry is not proof it eventually starts a run. The review shows active runs, pending accepted entries, continuations and unresolved effects. A known wake time is an observation, not a promise that the run completes then. A manually paused workflow or stopped trigger remains stopped after the change. Let a blocking run finish when its remaining work is still wanted. **Terminate run** stops that run, its child runs and remaining email follow-ups. Those do not resume later. Already accepted emails or external actions cannot be undone. Pausing the workflow does not finish its existing work or resolve an uncertain external result. ## Apply and follow space availability When checks require a decision, use the action the review offers. **Save change** applies a reviewed change; **Pause space and save** starts the final switch for a prepared change. A paused fresh review can require **Apply and reopen space** after you inspect its checks. | Status | What it means | | ---------------------- | --------------------------------------------------------------------------- | | **Draft** | The proposal still needs its review | | **Ready to save** | A reviewed proposal exists; inspect blockers and application controls | | **Preparing data** | Proposed values are being prepared; the current setup can remain live | | **Preparing to apply** | Accepted work is finishing before the change switches | | **Applying changes** | Publication and service checks are in progress | | **Needs a new review** | Resolve the reported problem and prepare current checks | | **Applied** | Definitions committed; also inspect availability and resumption | | **Cancelled** | The proposal was cancelled; inspect any remaining reopening or release work | Read the availability panel beside these statuses. Some changes preserve normal traffic during preparation, then pause the space for the final switch. During draining or a pause, new record, event or send requests can be refused and need a retry. Refused requests are not saved for later. Accepted work and retained workflow entries follow their own completion rules. Publication makes the prepared values and saved rules current together. Once that switch commits, recovery must finish; cancellation is not a rollback of the committed setup or of actions already accepted. Inspect the offered action and current state before trying to stop an application. No fixed pause duration is promised. The review can show a review-window deadline, but reopening and recovery can take longer. Keep the change page available to the person coordinating producer retries and customer automation. ## Recover an uncertain save or application While **Waiting to save** is shown, the save has not started and the app retries admission. Keep the page open. Once a change exists, inspect that existing change instead of repeatedly submitting copies. After an uncertain application response, choose **Check Apply status** when offered. This checks whether application started; it does not apply again. Read the returned state, blockers and availability before choosing the next action. **Cancel change** abandons an unapplied proposal. **Cancel and reopen space** requests release of a paused change. Cancellation can still require reopening and hold release. Do not assume normal producer traffic resumed because you clicked the cancel control. If source data changed, refresh the review. If a current check failed transiently, retry its checks. If the intended business meaning changed, edit the proposal and prepare a new review. These actions solve different problems; a retry should not silently change your conversion policy. ## Inspect the completed result The completion panel distinguishes **Applied. Space available.**, **Applied. Space is not available yet.**, and **Applied. Some work is waiting to resume.** Inspect updated-record counts and workflow checks, then open **View automation status**. Check event intake, workflow holds, unresolved external action results and observed runs. **No runs observed yet** does not establish that workflow execution succeeded. The displayed run counts cover observed activity and may be bounded; they do not guarantee future runs. Reopen representative records and inspect the actual field values. Test an inclusion, an exclusion and a missing value in the affected audience. For a scheduled evaluated field, check **Data through** and publication readiness separately. A committed definition is not the same as a completed fresh evaluation or delivered campaign. ## Related * [Attributes](/attributes#what-you-can-change-later-and-what-you-cannot): field settings and conversion controls. * [Events](/events): native occurrences, collection bindings and producer identity. * [Attribute expansion](/attribute-expansion#know-how-fresh-the-value-is): on-demand reads and captured publications. * [Segments](/segments): inspect audience meaning and shared dependencies. * [Workflow runs](/workflows/run): diagnose remaining customer actions. --- # Root domain vs subdomain for sending email Source: https://docs.maxclicks.ai/subdomain # Root domain vs subdomain for sending email Send marketing email from a subdomain such as `mail.company.com`; sending from a root domain such as `company.com` also works. A subdomain keeps the marketing sender and its DNS records easier to operate separately, but it does not fully isolate reputation, because Gmail evaluates some sender requirements across the whole primary domain. maxclicks verifies a root domain and a subdomain the same way, and a domain's name is fixed once added, so choose before you start sending. ## Why A dedicated subdomain makes the marketing sender and its DNS configuration easier to operate separately. It does not guarantee reputation isolation. Gmail evaluates some sender requirements across the primary domain, including its subdomains; review [Google's dashboard scope](https://support.google.com/mail/answer/14668346). maxclicks nudges you toward this. When you type a root domain into **Add domain**, a warning appears suggesting the `mail.` version instead, and the field's own hint reads "We recommend a subdomain for most companies but sending from your root domain also works." Common choices: * `mail.company.com` * `updates.company.com` * `hey.company.com` ## What your recipients see The verified domain is everything after the @ in your from-address. A [sender profile](/domain) called `support` on `mail.company.com` sends as `support@mail.company.com`, and that is the address sitting in the inbox. So pick a subdomain your audience will read as obviously yours. `mail.company.com` reads as your company. `m1.company.com` reads as a mailing list vendor. ## Nothing else changes A subdomain and a root domain go through exactly the same setup: add it, paste the DNS records into your registrar, start the check. maxclicks creates the bounce-handling subdomain under whichever one you chose. Walk through [domain setup](/domain) for the steps. ## Changing later A domain's name is fixed once it is added. To move to a different subdomain or to the root domain, add the new one alongside the old and verify it, then update the sender selection in the emails that should use it. Check live templates, future broadcasts and workflow drafts separately; republish workflow changes before expecting new runs to use the new sender. Existing published runs keep their version. Leave the old domain in place while work that still depends on it finishes. Treat a domain change as a sending change to monitor. Introduce volume gradually and inspect delivery errors and spam reports, following [Google's sender guidelines](https://support.google.com/mail/answer/81126). [Google Postmaster Tools](/postmaster) is the place to watch how Gmail is treating a new domain. Whichever you choose, leave the DNS records in place for as long as you send. If they are removed, maxclicks un-verifies the domain and every send from it stops. --- # Templates Source: https://docs.maxclicks.ai/templates # Templates A template lets your marketing team own the message while your product determines the moment it is sent. Build the subject, content, sender and customer data requirements in maxclicks; your application triggers the saved email for a recipient through REST or SMTP. Templates have no campaign audience or schedule. There is no separate publication step: a new send uses the saved template configuration. Coordinate edits to a template used by a live application, because saving changes affects subsequent requests. An email already rendered for an attempt is not rewritten by a later edit. For a one-time send to a whole audience use a [broadcast](/broadcast), and for a series that runs on its own use a [workflow](/workflows). ## Start with one recurring message For an order confirmation, your product knows the order that just completed. Create a **Transactional** template with a customer recipient and an order input, write the confirmation, and preview it with a real customer and order. Give your developer the template ID and those input keys. Each completed checkout then requests that saved message with its own customer and order identifiers. The template is reusable content for application sends, not a shared design automatically linked to every broadcast or workflow email. Those sending contexts own their own email content. Follow [Reuse a template](/guides/templates) for the complete handoff. ## Build the template Press **New template** on the Templates page. maxclicks asks two questions: which [editor](/editor) you want to write in, and the sending mode, marketing or transactional. The sending mode cannot be changed afterwards. The editor allows a few one-way moves: **Switch editor** in the composer takes a visual builder to a code editor, or the Dynamic Composer to Simple HTML, and none of them can be undone. Then the **Composer** tab is the [email editor](/editor), where you write the content, set the sender, and preview it. ## Decide what the email needs to know Every template carries a list of the information it expects, and it can only reference what is on that list. Open the **Data** tab in the right rail to define the expected data and the recipient path. Use **Personalize** for the model and generation controls, and **Checks** to review outstanding readiness issues. A receipt, for example, might expect three things: the `customer` it is addressed to, the `order` it is about, and a plain `tracking` value carrying the carrier name. The first two are [records](/schemas), pulled fresh from your data at send time; the last is a value you hand over with the send. One entry on that list is marked as the recipient, under **To**. It has to be a contact, or, for transactional email only, an email address you pass in. ## Send it The **Send** tab is the fastest way to wire this up. It shows the list of what the template expects with your own key names, marks which one is the recipient, gives you a ready-made example request with those keys already filled in, and shows the template's ID for copying. The [API reference](/api) covers the send call itself, the SDKs, and the idempotency key that stops a retry from sending twice. For record entries, pass identifiers for the customer or object the email should use. The send operation resolves and upserts those records, then reads their stored data and the configured attribute expansions for rendering. For plain JSON entries, send values matching the declared shape. A record reference and a JSON value are different inputs: agree their keys and types with the developer before connecting the trigger. Sending writes to your data. Every record you pass is saved before the email is built, so sending with a first name attached also updates that contact's first name. Pass identifiers, and pass fields only when you genuinely mean to change them. ### Make retries a shared contract Agree an [idempotency key](/api/idempotency) for each business message, such as the receipt for one order. Preserve the same key and payload when your application retries that logical send. A new key means a new operation and can send another email. The API response includes the email ID and send status. HTTP success alone is not evidence of delivery: inspect the returned status, then use email activity for later delivery events. A failed or uncertain outcome may still follow completed data writes or a provider attempt. Reconcile the email before asking the application to send it again. ### Review a live change Before saving a change to an active template, preview realistic examples, confirm the **Data** contract still matches the application, inspect links and consent mode, and use **Send test**. A preview of one customer cannot prove the data for every future request will be complete. If you need a separate rollout, create a separate template and coordinate the template-ID switch with the application owner. ## Who actually receives it Before every send maxclicks checks the recipient's [consent](/contacts) and your [suppression list](/contacts). * **Marketing** email reaches only contacts who subscribed to email, and to the topic if the content is bound to one. It is skipped for applicable channel-wide suppressions and the selected topic’s address opt-out, and it carries an unsubscribe link. * **Transactional** email can use a contact or a valid direct address without a marketing subscription, and has no marketing unsubscribe link. Hard-bounce and repeated-soft-bounce suppressions still block delivery. ## Why a send is refused Use the refusal reason to choose the next action: | What happened | What to do | | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | The data you sent does not match what the template expects | Compare your keys against the Send tab's list | | The template is missing a recipient, a sender, or content | Open **Checks** and follow the readiness issues in the composer | | The sending domain is not verified | Finish [domain verification](/domain) | | Marketing content was addressed to a bare email address | Supply a consenting contact as the recipient. Keep the sending mode aligned with the message’s purpose. | | The contact has not consented to the channel or the topic | Nothing to fix here: this is consent working | | The recipient is suppressed | Check the [suppression list](/contacts) | | Credits, allowance or workspace sending status prevents sending | Review [billing](/billing) and the workspace’s sending notices | ## Watch what happened Once the template is ready to send, two more tabs appear. **Emails** lists every email this template sent with its delivery state, and **Metrics** totals them up: delivered, opened, clicked, unsubscribed, and the rate behind each. An individual provider-accepted message has Sent status; Delivered is a later event, and neither proves inbox placement. The template funnel’s Sent total counts its linked email records, including unsuccessful ones. [Report definitions](/guides/reports#read-the-rate-and-its-denominator) explain that scope and the rate denominators. Rendering with Adaptyle adds usage separately from the email allowance, including applicable corrections and checks. The public templates API lists, reads and sends saved templates; it does not create their content. Build and review the email in the app or with maxinja. The app's readiness check expects valid sample data and a successful preview. Actual sends validate the supplied data and current sending prerequisites, then render for the real recipient. ## Remove a template Use **Delete** in its row menu when your application no longer needs the template. Deletion is permanent; a later request using that template ID cannot use the removed content. Coordinate the application’s trigger or replacement ID first. Removing the saved template also removes its template-level reporting associations. It cannot recall mail already delivered. ## Related * [Email editor](/editor): where the content, sender and recipient are set. * [Contacts](/contacts): identity, consent, and suppressions. * [Schemas](/schemas): the shape of the records a template references. --- # Transactional vs Marketing Email Source: https://docs.maxclicks.ai/types-emails # Transactional vs marketing email Choose the sending mode according to the purpose of the message. Marketing email promotes or nurtures a customer relationship; transactional email supports a specific customer action such as a receipt or password reset. The mode determines the consent and unsubscribe behavior applied when sending. The choice belongs to the content, not to how you send it. A [template](/templates), a [broadcast](/broadcast) and a [workflow](/workflows) can all send either kind, and no send call can override what the email itself says it is. **Sending mode cannot be changed after the email is created.** If the purpose requires a different mode, create a new email with that mode and review its sender, recipient and content. For example, “Your order is confirmed” is a transactional message about a completed purchase. “Try our new roast” is marketing, even when an application triggers it immediately after checkout. Sending through the API does not make promotional content transactional. ## Choosing the sending mode The picker offers two cards, in the app's own words: * **Marketing**: "Only subscribed contacts; manages the unsubscribe link." * **Transactional**: "All contacts regardless of subscription; no unsubscribe link." Newsletters, announcements, promotions, win-backs and re-engagement are marketing. Password resets, receipts, order confirmations, shipping notices and account alerts are transactional. Never mark promotional content transactional to get around the consent check. Transactional email skips consent by design, so doing this sends marketing to people who never agreed to receive it. Keep the mode aligned with the purpose and permission for the message. ## What actually changes | | Marketing | Transactional | | ----------------------------------------------------------------- | ------------------------------------------------------------ | -------------------------------- | | Recipient must be a contact | Yes | No, a bare email address is fine | | Contact must have consented | Yes, to email and to the topic if one is set | No | | Unsubscribe link in the footer | Yes, plus a one-click unsubscribe header for Gmail and Yahoo | No | | Blocked by an unsubscribe, complaint, manual or legal suppression | Yes | No | | Blocked by a bounce suppression | Yes | Yes | Everything else in the footer is the same for both: your socials, footer text, disclaimers, copyright and your business address, from the [brand kit](/brandkits) and brand in use. On the free plan every email also carries a small "powered by maxclicks" logo below that footer, marketing and transactional alike, linking back to the maxclicks site. It disappears as soon as the space is on a paid plan, see [billing](/billing). ## Consent, for marketing email A marketing email goes out only when the contact has agreed to receive email from you. If the email also carries a topic, they have to have agreed to that topic too. Marketing also needs a real contact. Sending marketing content to a bare email address that is not a contact yet is refused, because there is nobody to check consent against and no working unsubscribe link to give them. ### Topics A topic narrows consent further, so someone can take your product updates and skip your promotions. Set **Topic** in the email-level **Delivery** settings in the right rail. It offers **No topic** or the email topics defined in **Settings**, **Topics**. The recipient header in the rich composer also exposes the topic selection. Topics only apply to marketing email, and the field is hidden on a transactional email for that reason. The topic belongs to the template, alongside its sending mode. Check it explicitly when copying or reviewing content; consent to email alone is insufficient for a topic-bound message. A topic opt-out also records an address block for that topic within the workspace. It protects the address even through another contact with the same email. Rejoining that topic through valid preferences clears that topic block, while channel-wide suppressions and email subscription remain separate checks. ## Suppressions A suppression blocks an email address, not a contact, so it covers every contact using that address. Any applicable channel-wide suppression stops marketing email. For a topic-bound message, a block for that address and topic also stops it. Only a bounce suppression stops transactional email, so a password reset can still reach someone who left marketing while a bounce-blocked address remains ineligible. Hard bounces and spam complaints create suppressions on their own, and unsubscribe every contact on that address. See [contacts](/contacts). ## Why a send gets refused maxclicks checks these requirements before dispatch. A direct template send can be refused, a workflow can fail at its Send Email step, and a broadcast can exclude or reject an ineligible recipient. Inspect the response or email history in the sending context: | What you see | What happened | | ---------------------------- | ------------------------------------------------------------------ | | Marketing requires a contact | Marketing content aimed at an address that is not a contact | | Recipient not subscribed | The contact has not agreed to email, or to this topic | | Recipient suppressed | The address is suppressed | | Domain not verified | The sending [domain](/domain) is not verified yet | | Email limit reached | The space has used up its email allowance, see [billing](/billing) | If the email clears all of that and then fails at the mail provider, it is recorded as a failed send with the provider's reason attached, which you can read on the email itself. For the exact codes and status numbers behind these, see the [templates](/api/templates) API reference and [error handling](/api/errors). --- # Webhooks Source: https://docs.maxclicks.ai/webhooks # Webhooks Keep another system informed when customer records or email activity change. For example, send delivery and click events to your reporting service, or start a maxclicks workflow when your backend receives a business event. * **Outgoing**: maxclicks POSTs signed JSON to your URL when a record changes or an email event happens. You manage these in the app under **Settings**, then **Developers**, then **Webhooks**, or over the API at `/v1/webhooks`. * **Incoming**: your system POSTs to a [workflow](/workflows) trigger URL to start a run. ## Connect your first endpoint Have your developer prepare an HTTPS endpoint that [verifies signatures](#verify-the-signature), records each delivery once and returns a successful response. 1. Open **Settings → Developers → Webhooks → Create webhook**. 2. Enter the receiving URL. For email reporting, choose **email event** and the states you need, such as **delivered** and **clicked**. Choose the contact schema if you want to restrict the scope. 3. Save the webhook and copy its signing secret for your developer. It is shown only on creation or rotation. 4. Open the webhook and use **Test**. Check the response and the received payload. This is a sample event, not evidence that someone opened or clicked an email. 5. Inspect **Recent deliveries** when real activity occurs. Match the delivery ID in maxclicks with your receiver's saved entry. An outgoing webhook reports what happened. It does not update the receiving system unless that system implements the corresponding action. To start a journey from another system, use [incoming webhooks](#incoming-webhooks). ## Triggers An outgoing webhook connects one trigger to one URL. You set the trigger when you create the webhook, and it cannot be changed afterward. | Trigger | Fires when | `schemaId` | | ------------------ | ---------------------------------- | ------------------------ | | `contact upserted` | a contact is created or updated | contact schema, required | | `contact deleted` | a contact is deleted | contact schema, required | | `object upserted` | an object is created or updated | object schema, required | | `object deleted` | an object is deleted | object schema, required | | `event fired` | an [event](/events) fires | event schema, required | | `email event` | an email reaches a lifecycle state | contact schema, optional | `upserted` covers both creation and every update. There is no trigger that fires on creation alone. For `email event`, list the states you want in `emailEventTypes`, which cannot be empty: `scheduled`, `sending failed`, `sent`, `rejected`, `unsubscribed`, `bounced`, `complained`, `delivered`, `opened`, `clicked`, `delivery delayed`. `schemaId` is optional here, so you also receive events for email sent to plain addresses that are not contacts. ## Conditions A `condition` narrows down which records fire the webhook. | `type` | Effect | | --------------- | --------------------------------------------------------------------------------- | | `none` | Every record in the schema fires it. | | `segment` | Only contacts in the `segmentId` [segment](/segments) fire it. | | `custom filter` | A filter maxclicks builds from `requirements`, which you write in plain language. | Writing a custom filter calls AI, and it counts against your AI rate limit. Your credits are checked first (`402 insufficient_credits`), and only the readable description is stored: the filter maxclicks generates is never returned. For a contact-upsert trigger with a condition, the webhook responds when a contact enters the matching state and stays quiet on later saves while it still matches. With **none**, every creation or update can trigger it. Deletion conditions are checked against the deleted record's data. A broad trigger can therefore turn an import or bulk edit into many deliveries. ## Delivery maxclicks sends a `POST` with the raw JSON event as the body and these headers. A retry preserves the delivery identity: | Header | Value | | --------------------- | ------------------------------------- | | `X-Webhook-ID` | Delivery id, `wr_...` | | `X-Webhook-Timestamp` | ISO 8601 time the delivery was signed | | `X-Webhook-Signature` | `v1,` | | `Content-Type` | `application/json` | | `Idempotency-Key` | Same stable delivery ID | | `X-Maxclicks-Attempt` | Current attempt number | The body is the event itself. Unlike API responses, it is not wrapped in a `data` object. What is in the body depends on what happened: ```json // contact upserted / deleted { "contact": { "id": "...", "email": "jane@example.com", "firstName": "Jane" } } // event fired { "event": { "id": "...", "eventId": "order-9981", "createdAt": "2026-01-15T10:30:00.000Z" } } // email event { "contact": { "id": "..." }, "email": { "id": "..." }, "event": { "type": "opened" } } ``` Records carry stored attributes only. [Evaluated attributes](/evaluated-attributes), the ones maxclicks works out on the fly, are left out. Public record reads do not automatically expand computed values either. Agree the explicit stored data your receiver needs before relying on a field in the payload. ## Verify the signature Check the signature on every delivery, so you know it came from maxclicks. `X-Webhook-Signature` is `v1,` followed by the hex HMAC-SHA256 of `id.timestamp.body`: the delivery id, a literal `.`, the timestamp, a literal `.`, then the exact request body. The key is the webhook's `signatureVerificationSecret` (`whsec_...`), returned once when you create the webhook and again when you rotate the secret. Compute the HMAC over the raw request bytes, before any JSON parse or re-serialize. Sign the full `id.timestamp.body` message, not the body alone. ```js import crypto from 'node:crypto' function isValidDelivery(headers, rawBody, secret, now = Date.now()) { const id = headers['x-webhook-id'] const timestamp = headers['x-webhook-timestamp'] const signature = headers['x-webhook-signature'] if (![id, timestamp, signature].every((value) => typeof value === 'string')) return false const signedAt = Date.parse(timestamp) if (!Number.isFinite(signedAt) || Math.abs(now - signedAt) > 5 * 60 * 1000) return false const expected = 'v1,' + crypto .createHmac('sha256', secret) .update(`${id}.${timestamp}.`) .update(rawBody) .digest('hex') const received = Buffer.from(signature) const computed = Buffer.from(expected) return ( received.length === computed.length && crypto.timingSafeEqual(received, computed) ) } ``` Pass the raw request body as a Buffer. This example rejects missing headers, invalid timestamps, and timestamps more than five minutes from your server clock. Keep your clock synchronized and deduplicate accepted deliveries by `X-Webhook-ID` before applying side effects. ## Reliability Deliveries can arrive out of order and can be repeated. Persist the event durably, deduplicate by `X-Webhook-ID`, answer `2xx` promptly, then perform slower work from your own queue. Do not acknowledge before your system has safely recorded the work. Network failures, `408`, `429` and `5xx` responses are retried with backoff, up to five attempts within a 24-hour delivery window. `Retry-After` is honored for retryable responses when the delay fits that window. Other non-2xx responses finish the delivery as failed. This is bounded recovery, not a guarantee your endpoint eventually receives every event. The event payload, destination URL and signing secret are saved when delivery is queued. Retries keep that snapshot and delivery ID, while each attempt receives a fresh signature timestamp. Changing a webhook URL or secret does not rewrite queued deliveries. Keep the previous verification secret available while old deliveries drain. The consecutive-failure counter tracks terminal unhealthy deliveries, not each retry attempt. Five consecutive unhealthy completions trip the webhook to **circuit breaker** and stop further dispatch. Successful completion resets the counter. Exhausted `429` throttling and work suppressed because the webhook was paused do not increment that unhealthy streak, although the delivery can still be listed as failed. A tripped webhook stays off until you turn it back on. In the app, open the webhook and press **Reset and resume** on its circuit breaker card; over the API, `PATCH` its `status` back to `live`. Either way the failure counter is cleared. Pausing or tripping the webhook can suppress pending deliveries; resuming is not a replay of that missed history. Reconcile the affected records with your receiving system. Every delivery is listed under **Recent deliveries** on the webhook's page: open one to read the payload that was sent, the response body that came back, the response code, the duration, and the error message if it failed. ## Manage webhooks Webhooks live in the app under **Settings**, then **Developers**, then the **Webhooks** tab. **Create webhook** is there, and opening one shows its configuration and its recent deliveries, with **Test**, **Pause** or **Resume**, **Edit**, **Rotate secret** and **Delete** on the same page. **Test** builds a sample payload for the trigger, posts it to your URL, and shows you the status code, headers and body that came back, which is the quickest way to check an endpoint before real traffic reaches it. The API does the same work: | Method | Path | Purpose | | -------- | --------------------------------- | ----------------------------------------------------------------------------------------- | | `GET` | `/v1/webhooks` | List webhooks. | | `POST` | `/v1/webhooks` | Create a webhook. Returns the secret once. | | `GET` | `/v1/webhooks/{id}` | Get a webhook. | | `PATCH` | `/v1/webhooks/{id}` | Update `url`, `emailEventTypes`, `condition`, or `status`. | | `DELETE` | `/v1/webhooks/{id}` | Delete a webhook. | | `POST` | `/v1/webhooks/{id}/rotate-secret` | Issue a new secret for newly queued deliveries; older queued work keeps its saved secret. | `trigger` and `schemaId` are fixed once the webhook exists, and trying to change them returns `400 webhook_immutable_field`. An update can set `status` to `live` or `paused`, nothing else. Webhooks that belong to an [integration](/integration) (they carry an `integrationVendorName`) return `403 webhook_integration_owned` on update, delete, and rotate: change those in the integration. `signatureVerificationSecret` is shown on create and rotate-secret only. Save it immediately. ```bash {{ title: 'cURL' }} curl -X POST https://api.maxclicks.ai/v1/webhooks \ -H "Authorization: Bearer $MAXCLICKS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/hooks/maxclicks", "trigger": "contact upserted", "schemaId": "students", "condition": { "type": "none" } }' ``` ```js {{ title: 'Node' }} const webhook = await mc.webhooks.create({ url: 'https://example.com/hooks/maxclicks', trigger: 'contact upserted', schemaId: 'students', condition: { type: 'none' }, }) // webhook.signatureVerificationSecret is returned only here. ``` You get back the webhook itself, with `id`, `url`, `trigger`, `emailEventTypes`, `schemaId`, `condition`, `status`, `totalCalls`, `failedCalls`, `failedCallsInARow`, `lastCalledAt`, `lastCallErrorMessage`, `integrationVendorName`, `createdAt`, and `updatedAt`. ## Incoming webhooks Your own system can start a [workflow](/workflows) run by POSTing to an incoming-webhook trigger step. Copy the URL from that step in the app: ``` https://api.maxclicks.ai/v1/workflows/{workflowStepReference} ``` POST your JSON body. maxclicks checks it against the step's JSON schema and starts a run, with your body as the data the run begins with. A success returns `200` and an empty `data` object: `{ "data": {} }`. This endpoint needs a `Bearer` [API key](/api/authentication) for the workflow's space. Requests without one are rejected. Send an `Idempotency-Key` for the logical trigger and preserve it when retrying. Follow the [idempotency guide](/api/idempotency) if the outcome is unresolved. If the step does not exist, is not published, or is paused, you get `404 workflow_step_not_found`. A body that does not match the step's schema returns `400`. --- # Workflows Source: https://docs.maxclicks.ai/workflows # Workflows A workflow follows up when a customer needs it and stops when they have already acted. For example, wait two days after signup, check whether that person created a project, and email only those who have not. maxinja can build the journey from your brief; you review its decisions on the canvas. A **trigger** starts the journey. **Steps** read data, make decisions and take action. A **run** is one execution of that journey, with a history you can inspect. Publishing makes a version live; editing a draft does not change an existing published version. Start with [Build a signup follow-up](/guides/workflows) for a complete example. This page explains the choices behind it. ## Choose a step for the customer decision | What you need to know or do | Use | Why | | ---------------------------------------------------- | ------------------------------------ | ----------------------------------------------------------------------------------- | | Has this person created their first project? | **Query**, then **If/Else** | Read the records and branch on the answer. A count does not need AI interpretation. | | Which contacts match this event or audience? | **Find Records** | Bring the matching contacts into the run. | | What first project fits this person’s stated goal? | **AI** | Interpret the supplied context and pass a structured answer to later steps. | | Change a passage inside the email for each person | **Adaptyle** in **Send Email** | Write instructions in the email; preview the finished message for a recipient. | | Has a particular activity happened in a time window? | **If/Else → Customer event history** | Check the event record with identity matching and an allowance for late events. | | Start when a calculated value crosses a boundary | **Evaluated attribute threshold** | React to a published background computed value crossing the selected threshold. | Use a [broadcast](/broadcast) when you want one reviewed audience to receive a one-time message. Use a workflow when the next action depends on when an individual qualifies and what happens along the way. ## Define the journey before the steps Use a brief that names the starting event, customer, timing, decision and message. For example: "After a signup event, wait two days. Check whether that person has created a project. If they have none, prepare an email helping them start their first. Keep the workflow unpublished for review." Before publishing, inspect the event-to-customer relationship, the condition after the delay, the recipient data and a sample email. A teammate's activity should not qualify as this person's activity unless that is the behavior you intend. ## Build one Open **Workflows** in the top navigation, then **New workflow**. An empty canvas opens. ### Add a trigger Every workflow starts with one. It decides what sets a run going: a record created, an event fired, an email opened, a date and time, or a call from your own code. ### Add the steps that follow Connect a step under the trigger, then another under that. Steps look things up, wait, branch, write records, and send email. ### Tell each step what to do Select a step and describe what you want in plain language. maxinja turns it into the logic and shows you a readable summary to confirm. ### Publish The **Publish** button appears once you have unpublished changes. Publishing checks the whole workflow, then makes it live. Step-by-step guidance for the canvas itself is in [Create a workflow](/workflows/create). ## Steps There are 21 step types, in five categories. Each one reads the values earlier steps collected and adds its own. | Step | Group | What it does | | ----------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------- | | Scheduled Timestamp | trigger | Runs once at a date and time you set, or right after publishing when you set none. | | Record Created or Updated | trigger | A contact or object record is created or updated. Pick the schema, and optionally a [segment](/segments) or a condition. | | Record Deleted | trigger | A contact or object record is deleted. | | Event Fired | trigger | A workspace event schema fires, or a selected collected-event definition matches; collected definitions are pinned at publication. | | Email Event | trigger | Email activity anywhere in your space: sent, delivered, opened, clicked, bounced, complained, unsubscribed, delayed, and more. | | Evaluated attribute threshold | trigger | Starts when a Scheduled evaluated attribute crosses the comparison you choose; publication establishes a baseline. | | Incoming Webhook | trigger | An outside system posts JSON to this step's own URL. | | Find Records | enhancer | Pulls contact or object records into the run: all of them, a segment, or a filter you describe. | | Set Value | enhancer | Works out a value from what the run already carries, with no database lookup. | | Query | enhancer | Runs a database lookup, from a simple fetch to a count or a sum, and adds the answer. | | AI | enhancer | Runs your prompt over the run on a model you pick, and adds its answer. | | For Each | enhancer | Splits the run over a list, carrying on with one run per item. | | If/Else | controller | Sends the run down the **If true** or the **If false** path. | | Switch | controller | Sends the run down one of several named case paths. | | Delay | controller | Holds the run for a duration, or until a moment. | | Create or Update Records | action | Builds contact or object records from the run and writes them, merging into existing records or creating new ones. | | Delete Records | action | Deletes contact or object records. | | Fire Event | action | Fires an event, which other workflows and [webhooks](/webhooks) can trigger on. | | HTTP Request | action | Calls an outside URL and adds the response to the run. | | Send Email | action | Sends this step's own [email](/templates), and can carry on when that email is opened, clicked, and so on. | | Note | decorator | A note on the canvas for whoever opens this workflow next. It takes no part in a run. | One more card appears on the canvas that is not in the list: **None**, an empty step maxclicks creates when you add a step from an outgoing connection point. You never pick it from the step type list, and a workflow still holding one will not publish. [Workflow blocks](/workflows/blocks) covers every type in full, with what you configure on each. ### You describe the logic, maxinja writes it You never hand-write a condition, a filter, a value or a query. You type what you want, maxinja compiles it, and the step shows you a plain-English summary of what it will do. If the summary is not right, refine it and compile again. On an If/Else, Switch or Delay, uncompiled generated logic is evaluated by a model at execution. This adds model usage and requires an explicit model and thinking setting before publication. Other generated logic fields must be compiled. The dedicated AI step always calls a model; fields such as an email recipient or webhook payload are configured separately. See [step logic and AI](/workflows/ai). ### One run per record, or one run for the whole list Find Records, Create or Update Records and Delete Records each have a **Process each record separately** checkbox. Checked, the run fans out: each record carries on independently, and later steps see one record at a time. If nothing matched, the run ends there. Unchecked, the run stays a single run carrying every record together as one list, and it carries on even when nothing matched. Leave it off when a later step has to act on all the records at once. A For Each step always fans out, one run per item. ## Slots and links Steps join up through the connection points on their edges: a step takes its input at the top and hands the run on from its outputs. * A step's outputs are its **slots**. Most steps have one. An If/Else has two, **If true** and **If false**. A Switch has one per case. * A **link** joins one slot to one step, and a slot can only feed one step. * Every step except a trigger and a Note needs at least one incoming link. * A trigger cannot have anything linking into it. * A workflow cannot loop back on itself. The editor flags the loop with **Currently, cycles are not supported.** When several links arrive at the same step, that step can only count on values that all of them carry. Rewire things so a step loses a value it was set up to use, and the editor flags it until you fix it. ## What a run carries A run carries its own values, each under a name you choose. The trigger puts the first one there, such as the event that fired. Every step after it reads what is already there and adds more, so a Send Email step near the end can use everything the earlier steps collected. That is why order matters: a step can only use what something before it put there. ## What it costs A trigger that fires per record starts one run per record. A Record Created or Updated trigger on a schema with 50,000 contacts, set off by a bulk import, is 50,000 runs. AI, metered tools and sending usage depend on the steps each run executes. Standard internal workflow steps have no fixed operation fee. AI steps and uncompiled If/Else, Switch or Delay decisions spend prepaid credits on model and metered tool usage. Send Email uses the shared recipient allowance, with separate charges for email overage and AI personalization. [Workflow billing](/workflows/billing) explains costs and concurrency limits. ## Trigger a workflow An **Incoming Webhook** trigger lets an outside system start a run. ### Generate the URL Select the step and use the generate button next to **Trigger URL**, then copy it. The URL only goes live once you publish. ### Describe the body Fill in **Expected payload** with the shape of the JSON you will send. maxclicks checks every incoming request against it and rejects anything that does not match, and later steps can then use the values. The workflow will not publish without it. ### Give the sending system the URL It sends an HTTP `POST` with a JSON body and an `Authorization: Bearer` header carrying an [API key](/api/authentication) for this space. Treat the URL like a secret. Renewing the URL gives you a new one, and the old one keeps working until the next publish. Whatever you post travels with the run as its starting values. The full request and response detail is in the [workflows API reference](/api/workflows). ## Related * [Create a workflow](/workflows/create): build the graph step by step. * [Manage workflows](/workflows/manage): publishing, pausing, versions. * [Workflow runs](/workflows/run): see what happened, and why a run failed. * [Workflow blocks](/workflows/blocks): every step type in full. * [Segments](/segments): reusable contact filters for triggers and Find Records. * [Evaluated attributes](/evaluated-attributes): on-demand customer facts and related-record lookups you can use in step logic. * [Scheduled evaluated attributes](/attributes#computed-attributes): background calculations and their published values. --- # Step logic and AI Source: https://docs.maxclicks.ai/workflows/ai # Step logic and AI Use a rule when the answer can be calculated. Use AI when the answer needs interpretation. In a signup journey, “has this person created a project?” is a lookup and a count. “What project fits their stated goal?” needs a recommendation. These are different jobs, with different costs and checks. maxinja can build either kind of step from your description. The difference is what happens after publication: saved logic runs directly; a runtime AI step calls a model as each customer reaches it. ## Choose the right approach | Customer decision | Approach | What to inspect | | ------------------------------------------------- | -------------------------- | ------------------------------------------------------------------------- | | Count this person’s projects after a two-day wait | **Query** | The owner relationship, the result shape and a valid zero result. | | Stop if the count is greater than zero | Compiled **If/Else** | The condition and which output continues to email. | | Recommend a first project from the person’s goal | **AI** | Supplied facts, the prompt, result shape and missing-context behavior. | | Rewrite an email passage for that person | [Adaptyle](/adaptyle) | The selected passage, its instruction and the finished recipient preview. | | Check whether a specific event occurred recently | **Customer event history** | Customer identity, lookback and late-event allowance. | The [signup workflow guide](/guides/workflows) connects Query, a compiled decision, AI and an email in one example. Start there if you want to build a journey before learning the configuration details. ## Describe what you want Select a step and its logic box is labelled for what it produces. | Label | Steps that have it | | -------------- | ---------------------------------------------------------------------------- | | Condition | Record Created or Updated, Record Deleted, Event Fired, Email Event, If/Else | | Filter | Find Records, Delete Records | | Value | Set Value | | Query | Query | | Case selection | Switch | | Delay | Delay | | Records | Create or Update Records | | Event data | Fire Event | Write one plain sentence in it. The greyed example in the box shows the register to aim for: "Customers who purchased in the last 30 days" on a Find Records filter, "The count of contacts created this month" on a Query, "Route by the order's plan tier: free, pro, or enterprise" on a Switch. An optional filter left empty can include every record of the selected type. Required decision and value fields still need configuration; an empty If/Else condition does not mean every customer takes the true branch. That default targets the entire schema on Delete Records. Deletion is permanent when it executes; the runtime rejects a match larger than 1,000 records. Scope the filter deliberately before you publish. Eight step types have no generated logic box: Scheduled Timestamp, Evaluated attribute threshold, Incoming Webhook, AI, For Each, HTTP Request, Send Email and Note. Neither does the empty placeholder. You configure those with ordinary fields. The info button beside the label explains what that particular box controls. It is also where warnings live: errors appear under the box, warnings only inside that info card. ### Point at the data the run carries Type `@` in the box and a picker opens listing everything the run carries at this point, with the step that produced each value. Choosing one drops a reference like `{{ contact.email }}` into your sentence. An amber marker means two earlier steps pass on the same name. [Evaluated attributes](/evaluated-attributes) do not appear in the picker, even though the logic maxinja writes can read them. Ask for them by name in your sentence. These on-demand lookups are distinct from background [Scheduled evaluated attributes](/attributes#computed-attributes), whose published generations can be pinned for an execution. ### Send it to maxinja Press Enter, or the round arrow button beside the box. maxinja opens with your sentence and the step attached, works through it, and reports what it changed. Typing in the box saves nothing on its own. Sending the sentence to maxinja is the only thing that stores it. Type a sentence, click away without compiling, and it is gone. maxinja may rename the step and rewrite its description in the same pass, so a step can retitle itself after you compile it. ## Read back what it wrote When it is done the box header reads **Compiled** with a green dot and opens to show three things: * **A plain-English description** of what the logic actually does. This is what you check, not code. * **Depends on**, listing the **Schemas**, **Attributes** and **Segments** the logic touches. A red **Unknown schema**, **Unknown attribute** or **Unknown segment** badge means something it relied on is gone. * **Reads**, a read-only view of the data the step takes in and the related records it loads with it. The generated logic itself never reaches your browser. There is nothing to view, copy or hand-edit: it stays on the server, and only maxinja can write it. What you review is the description and the dependency list. ## Refine it The box always shows the sentence that was saved, and you can edit it in place. While your edit is unsent, **Not compiled yet** and a **Revert** button appear under the box, and the header keeps reporting the state of what is actually saved rather than what you are typing. Send it again and maxinja replaces the whole thing, so rewrite the sentence rather than appending a correction. Two things to know about the box: * **You cannot empty it to remove logic.** An empty box will not compile. To drop a filter, ask maxinja, or pick a segment on the steps that offer one (Find Records, Delete Records, Record Created or Updated, Record Deleted), which replaces the box entirely and discards the sentence. * **The arrow greys out for a moment** right after you change another field on the step, because maxinja reads the saved step. Wait for the save to settle. For a smaller nudge, use the maxinja box at the top of the sidebar instead. It talks about the same step without replacing your sentence wholesale. ## Compile once, or decide on every run This is the distinction worth understanding before you build anything that runs at volume. | | Compiled once | Decided on every run | | --------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | | When the AI works | Once, while you build the step | Every time a run reaches the step | | What happens on a run | maxclicks runs saved logic without asking a model to interpret that rule. Configured data work is separate. | Your sentence and the configured data go to a model, which answers | | Execution | Evaluates saved logic over the configured data | Waits for a model result before continuing | | Cost per run | No fixed internal step fee or model interpretation of the saved rule; paid tools and email remain separate | Model and metered tool usage; no fixed internal step fee | | Repeatability | Same logic over the same inputs gives the same decision; time and live data can change the inputs | Separate model evaluations can differ | | Which steps | Every step with a logic box | If/Else, Switch and Delay only | | Publishing | Required, except on those three | Allowed, with a warning, once you pin a model | Compile a measurable rule such as an order-value comparison. Reserve a model evaluation for a decision that needs interpretation, and provide the evidence and permitted outcomes in the step's instructions. Compiled execution has no fixed internal fee; separate paid tools, model work and email overage still apply when used. ### What puts a step in each mode There is no switch to flip. A step decides on every run when it has a sentence and no compiled logic behind it. maxinja prefers to compile, and falls back only when the logic genuinely cannot be reduced to a rule. When it makes that choice it says so in its report. The visible signal is a required field group titled **AI on every run** appearing under the logic box. When a step is compiled, that group is not there. Its disappearance confirms that this controller no longer requires a model evaluation on each run. Standard internal execution has no fixed step fee. ### Only three steps can decide on every run If/Else, Switch and Delay. On those, leaving the logic uncompiled is a publish warning, not a blocker, and the workflow ships: > The condition is not compiled yet, so AI evaluates it on every run, adding latency and cost. Switch and Delay show the same sentence for their case selection and their delay. Every other step type refuses to publish until its logic is compiled, with a hard error such as **The filter is not compiled yet.** or **The query is not compiled yet.** A Switch has no fallback path. If the model lands on something that is not one of your cases, the run fails outright. Make the cases exhaustive, or add an explicit catch-all case and say so in your sentence. ### Pick the model and thinking depth The **AI on every run** group holds two required pickers, **Model** and **Thinking** (Off, Low, Medium, High). Both start empty, and the workflow will not publish until you set them: > Pick the model for the runtime AI call. > > Pick a thinking depth for the runtime AI call. There is deliberately no workspace default behind them. Changing the model in your workspace AI settings can never move what a published workflow does or what it costs. The trade-off, as the field itself puts it: "Deeper thinking gives better judgment, but makes every run slower and pricier." Choose from the models currently offered by the step. Use representative input and output complexity to compare speed and usage; model choice alone is not a fixed per-run price. ## Decide from customer event history **If/Else** offers **Customer event history** as an explicit alternative to generated condition logic. Choose the event collection, **Has occurred** or **Has not occurred**, **Look back (days)**, and the **Timing and matching** settings. The window is captured when the step reaches the decision and does not slide forward on every retry. **Allow late events (seconds)** waits beyond the captured window end before deciding. An absence condition requires at least 60 seconds of grace; the UI supports a lookback of 1–30 days and grace up to one day. **Match** can be **Events for this customer or object**, or **Same correlation ID as the triggering event**. The latter requires that the trigger actually supplied a correlation ID. The run also needs exactly one authoritative customer or object; an ambiguous subject is an error, not permission to check everybody's activity. If event storage is behind, the run waits for usable evidence. A decision that cannot be completed within its permitted lifetime fails rather than silently deciding that no event occurred. Once accepted, the decision and evidence are preserved for that step occurrence. Use this when the distinction between no activity and missing data matters to the email you are about to send. ## What it costs Standard internal step execution has no fixed fee. A compiled rule avoids model interpretation during execution. Runtime AI decisions, generation and metered tool calls consume prepaid workspace credits. No monthly AI allowance is included. The same journey can have very different costs depending on how many times its AI steps run and how much context they use. Count parent and child runs and estimate model/tool work before increasing enrollment. An AI step's cost depends on its input and output tokens, model, thinking and tools. A fixed per-recipient price inferred from one example is not reliable for a different journey. See [workflow billing](/workflows/billing) for the full charging model and the cap on concurrent runs. Cost also depends on the data configured for the step, including expanded relationships and attributes. Supply what the decision needs. Passing a large profile and related records increases input size compared with passing the one fact a rule actually uses. Billing has its own **Usage** tab: a spend chart at daily, weekly or monthly granularity over any date range, a summary, and breakdowns you can group by category or by member, with **Workflows** among the categories. What it will not tell you is which workflow, which run, or which step. Publish a workflow with a step deciding on every run and you will see the Workflows category move without being told what moved it. The runtime records the result of a model evaluation for its step occurrence. A completed If/Else, Switch, Delay or AI evaluation can be reused during safe recovery. An interrupted evaluation with an unknown outcome is not blindly rerun: the step requires reconciliation. A Delay also keeps its saved deadline rather than starting the wait again. ## Choose between them * **Compile anything you can state as a rule** over data the run already carries. "The order total is over $50" is a rule. Compiled, it avoids an AI interpretation on each execution and gives a repeatable decision over the same inputs. * **Leave it to a model only for genuine judgment.** "This product fits the customer's interests" is not a rule. That is what the per-run mode is for. * **Supply the evidence.** Ordinary controller logic evaluates the data configured for that step. Use Query or Find Records to bring other context into the run. If/Else also has a dedicated **Customer event history** mode for a bounded event-history decision, described below. * **Start cheap.** A fast model with Thinking set to Off fits most per-run work. Raise the model or the depth only for nuanced multi-factor reasoning, long or messy context, or an outcome the rest of the run leans on heavily. * **Compiling is a one-time cost.** It runs a strong model working through your workspace's data while you wait, which is why it is not instant. Later executions avoid that interpretation cost, while other model, metered tool and email overage costs remain. The benefit depends on how often the workflow runs and how complex the rule is. ## The AI step always calls a model The **AI** step has no compiled mode. It runs your prompt over the selected context and adds a structured answer that a later branch, record action or email can use. For the Loopwell signup example, the input is the user’s role and stated goal. The prompt asks for one first-project suggestion, with a generic answer when that context is missing. The result can be a text field named `suggestion`, passed on as `firstProjectGuidance`. The earlier Query and If/Else decide whether the person needs this message; the model does not guess their project count. Configure **Expected data**, **Prompt**, **Result shape**, and the **AI settings** group’s required **Model** and **Thinking**. The model is pinned to the step; changing workspace defaults does not change a published workflow. Inspect the input and result in [run history](/workflows/run) after execution. Matching the result shape establishes structure, not the quality of a recommendation. Check whether the answer uses the supplied facts, respects missing context and is suitable for its next use. The answer is run data, not an automatic profile update. If another campaign needs the recommendation, add **Create or Update Records** to save the chosen field on the identified contact. If it is only needed by this journey's email, pass the named output to that email with Liquid. The AI step does not automatically receive the later email's brand kit. Include the writing guidance it needs in its prompt or selected data. Inserting its answer with Liquid does not rewrite that answer in the email's brand voice. Missing customer context and failed generation need different treatment. A prompt can request generic guidance when a field is absent. It cannot recover from a model or output-validation failure: a terminal AI-step error ends the run, so a later email is not a fallback for that failed step. Review a controlled execution before publishing to a wider audience. Tool settings affect each execution, rather than authoring alone. * **Web search** requests a provider capability that the current runtime refuses before inference because no supported execution budget is available. Leave it unchecked for an executable step; a visible checkbox is not a successful web-search grant. * **Workspace data access** hands the model read-only query tools plus a written description of your data, which it reads every time. In the product's own words, it "makes every run markedly slower and pricier". * **Workspace data access** or selected **MCP servers** can turn one answer into an agentic loop of up to 15 model turns. Every new generation or metered call adds its applicable usage. Without tools, the step requests one structured answer. With tools, it can make several model turns and tool calls, up to the configured 15-turn bound. That is a limit, not a cost estimate. MCP tools may act on connected systems according to their permissions; review those capabilities as part of publishing the journey. ### Review MCP changes and uncertain outcomes MCP selection grants the connected server's tools to the AI step. The runtime captures its endpoint, credentials, tool definitions and instructions, and checks that they still match before using them. Changing a server's settings or tool contract can stop execution; the step does not silently continue with a different tool. MCP tools are not guaranteed read-only. A tool may change the connected service, and an external service can accept an action before its response is lost. Inspect that service and the run's recovery reason before triggering replacement work. **Needs attention** means the run does not automatically continue; **Terminate run** ends its lineage without reversing accepted effects. The workflow grants are bounded: at most 32 servers and 128 tools, with up to 64 tool calls for a decision. An individual tool input or result is limited to 256 KiB, and aggregate MCP response data to 4 MiB. Large tool catalogs, changed descriptors or oversized results can fail the step. Supply narrower context or a narrower tool contract rather than expecting an unrestricted loop. ## Personalized email bills per recipient A Send Email step has no **AI on every run** group, but it can still call a model: surviving [Adaptyle](/adaptyle) instructions are applied while that recipient’s message is prepared for sending. Personalization usage is per recipient, and its model and thinking settings live on the email itself. Adding both an AI step and Adaptyle means two separate generation tasks; use each for a defined purpose. Content with no Adaptyle instructions in it renders with no model call and no AI cost. Ordinary [Liquid](/liquid) substitution does not itself call a model; workflow fees, any configured data calculation and email allowance rules remain separate. ## Keep your steps compiled Compiled logic is dropped when the thing it was built against changes. After any of these, the step goes back to uncompiled and needs a new compile: * **Editing the sentence** and saving it. * **Changing the schema** the step is bound to. * **Adding, removing or renaming a Switch case.** Reordering them is safe. * **Picking a segment**, which replaces the box and discards the sentence. Renaming a Switch case can invalidate its compiled selection. Recompile and inspect the mode before publication. A controller left in runtime AI mode needs explicit model settings; do not treat a successful save as confirmation that the decision still runs the way it did before. When changing the data model or a referenced segment, inspect the dependent steps and the setup review outcome. A saved edit does not establish that every published workflow has adopted a new definition. Reopen the step, read **Depends on** and **Reads**, and regenerate affected logic when required before publication. ## Related * [Workflows](/workflows): the canvas, runs, versioning, and the trigger API. * [Workflow block library](/workflows/blocks): every step type and what you configure on it. * [Workflow billing](/workflows/billing): model and tool usage, email allowances, and the concurrent run cap. * [Workflow runs](/workflows/run): what a run did, and why one failed. * [maxinja](/maxinja): the assistant that compiles your step logic. * [Segments](/segments): reusable contact filters you can pick instead of writing a condition. * [Evaluated attributes](/evaluated-attributes): on-demand customer facts and related-record lookups. * [Scheduled evaluated attributes](/attributes#computed-attributes): background calculations with published values. --- # Workflow billing Source: https://docs.maxclicks.ai/workflows/billing # Workflow billing A journey's cost depends on how many runs start, how many steps they execute and where AI is used. Model and metered tool work spends workspace credits; email also draws on the shared sending allowance. Plan these components together before scaling a journey. ## What a step costs Standard internal step execution has **no fixed operation fee**. Models and metered third-party tools are charged by usage from prepaid credits, and email above the included allowance costs **$0.40 per 1,000 recipients**. | Step | Charge | | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | | Find Records, Set Value, Query, For Each and compiled If/Else, Switch or Delay | No fixed internal step fee. Any paid external service invoked is separate. | | AI | Model and metered tool usage. | | Uncompiled If/Else, Switch or Delay | Model usage when the rule is interpreted during a run. Compiling moves interpretation to authoring. | | Send Email | Recipient sends use the shared allowance; email overage and model-based personalization are separate. | Charges retain workflow execution context for accounting. In **Billing → Usage**, an admin sees spend by period, by category (**Workflows** is one) and by member. The screen does not break it down per workflow, run or step. Use run history to inspect execution volume separately. Free has a hard cap of 100 recipient sends each month. Premium includes 50,000, plus 10,000 for each paid additional record pack for a full month. Mid-month additions are prorated. Included email does not include AI personalization, and no monthly AI allowance is granted. ## Running out of credits When paid work cannot obtain credits, it waits and checks again. Adding credits can let that waiting work continue; creating a replacement run risks doing the work twice. Premium also allows a limited negative-credit buffer, so a displayed balance at zero is not a precise stop switch. Credit waits are different from terminal failures such as invalid step data, an unusable sender domain or exhausted email allowance. Topping up does not repair a run already marked Failed. Inspect the run reason first. Paid model/tool work is recorded for its execution occurrence. Safe recovery reuses settled receipts and recorded effects rather than charging them again. A new run, correction or deliberate new generation can create new paid work. An unknown model or external tool outcome can place the run in **Needs attention**, which does not continue automatically. Unresolved AI receipts can retain reserved credits while that outcome is reconciled. A balance change or top-up does not prove whether the action completed, and **Terminate run** does not refund paid work or reverse an accepted external effect. ## Estimate the work that reaches paid steps Use a planning cohort, then compare it with actual execution and usage. For an illustrative 1,000-signup cohort, suppose your post-delay Query finds 600 people already created a project. A branch that ends their journey leaves 400 people reaching the email and any downstream AI guidance. * Placing the guidance AI before that branch means it can run for all 1,000 people. Placing it afterward limits that task to the 400 who still need it. * Adding Adaptyle creates a separate recipient-rendering task for each email requiring generation, even when an earlier AI step already wrote guidance. * Giving the AI step workspace tools or MCP servers can create several model turns and tool calls per person. The 15-turn model bound is a ceiling, not the expected usage. * A Find Records selection or For Each list can create child runs, each executing its own later paid steps. Count those occurrences as well as the root cohort. These counts illustrate where usage arises; they are not predicted customer behavior or a price quote. Model input/output, thinking, tool work and the actual paths determine cost. Preview and authoring usage also spends credits before the workflow runs. Use the email editor's **Personalize → Estimate** for its recipient-rendering and sending estimate. It is not a quote for the entire workflow, upstream AI steps, MCP calls or future event branches. Check [the email estimate](/editor#read-the-estimate-before-scaling-up) and representative [run history](/workflows/run). ## Concurrent run cap Each space has a ceiling on runs active at once: 1000 plus 20 per object record. The cap scales with your data, so larger spaces run more in parallel. A step set to process each record separately creates child runs; For Each creates one per item. Work beyond the active-execution capacity waits for a turn. Durable sleeping delays do not occupy an active execution slot for their whole wait. The cap controls simultaneous execution, not the lifetime total of journeys you may start. The cap limits pressure on the workspace, but it is not a campaign budget. An indirect loop can still generate continuing work: workflow A fires an event for B, and B updates a record that triggers A again. Design conditions that stop that cycle and review fan-out before publishing. Record-selection limits are separate from capacity. Find Records can pass at most 1,000 records in one list; separate processing handles larger selections in child runs. For Each accepts at most 1,000 items, and Delete Records rejects a match over 1,000 records. Waiting for a capacity slot does not repair an oversized list or deletion selection. ## Control enrollment and execution costs **Stop new runs** closes new enrollment while existing runs finish. **Pause** also parks further execution. **Terminate run** ends one selected run, its descendants and pending continuations. These controls retain completed effects and incurred usage; none is a refund or campaign budget setting. After a pause, saved Delay deadlines can already have passed. Resuming or publishing can release waiting execution, including runs on older versions. Inspect those paths and their remaining paid steps before releasing a correction. Publication preserves a separate new-enrollment stop. ## Managing usage Admins on Free and Premium can review usage, buy credits and configure auto reload in **Billing & Usage**. See [AI and usage credits](/billing/credits) for rates, balances and top-up controls. Buying credits does not raise Free's email allowance. ## Related * [Workflows](/workflows): steps, versioning, and the trigger API. * [Workflow runs](/workflows/run): what each run did. --- # Workflow block library Source: https://docs.maxclicks.ai/workflows/blocks # Workflow block library Choose a block for the question you need answered or the action you need taken. A signup follow-up, for example, needs to identify the person, wait, check whether they started, and decide whether an email is still relevant. There are 21 step types in five categories. This page covers all of them: what each one is for, what you fill in, and what it leaves behind for later steps. It ends with the empty placeholder a step starts as before you give it a type. ## Find the right block | Your task | Block | | ---------------------------------------------------------- | ------------------------------------------------- | | Respond to signup or another business event | [Event Fired](#event-fired) | | Locate the contact from that event | [Find Records](#find-records) | | Give the customer time to act | [Delay](#delay) | | Check current records, such as this person’s project count | [Query](#query) | | Calculate from values already in the run | [Set Value](#set-value) | | Interpret a goal or prepare guidance for later steps | [AI](#ai) | | Continue only when a condition is met | [If/Else](#if-else) | | Check whether an activity happened in a defined window | [Customer event history](#customer-event-history) | | Send the email to this contact | [Send Email](#send-email) | | Act once for every item in a list | [For Each](#for-each) | For a complete journey using these blocks, follow [Build a signup follow-up](/guides/workflows). ## How to read an entry Every entry lists **Inputs**, the fields you fill in on the step's side bar, and **Outputs**, the values the step adds to the run. Outputs are the part worth reading first. A step passes its result on under a name, and later steps use that name to reach it. You set it in the collapsed **Rename what this step passes on** section, which holds two fields on every step that produces something: * **Name**: what later steps call this value. Each step suggests one, so you rarely touch it. The field's own note says it is *"Only worth changing when two steps would pass on the same name."* * **Note for maxinja**: an optional line describing the value, so the assistant knows what it is looking at. Two steps using the same name is not an error, and the editor does not stop you. The later step wins, and the earlier value is gone for everything downstream. The editor warns you and names the other step: **"… already passes on 'x', and this step overwrites it for everything downstream. Give one of them a different name."** Every step's side bar opens the same way: the maxinja composer at the top (**Build this step maxinja...**), then the step's title and an optional **Add description**, then the step's own fields. There is no save button. Edits settle on their own, and none of them touch anything live until you publish. Where a field asks you to describe logic in plain language, type `@` to pick a value the run carries rather than typing its name from memory. ## Triggers A trigger decides what starts a run. Every workflow needs at least one, nothing can connect into a trigger, and each has a single **Then** output. A trigger only listens while the workflow is published and not paused, and only the currently published version listens. See [Manage workflows](/workflows/manage). ### Scheduled Timestamp Starts one run at a moment you choose. **Inputs** * **Trigger time** (optional): pick a date and a time. Leave it empty to fire immediately once the workflow is published. Times are in your local time zone. **Outputs** Nothing. A run from this trigger starts empty, so the steps after it have to fetch whatever they need with a **Find Records** or **Query** step. **Notes** Publishing is what arms it, and it fires exactly once. Republishing arms it again, so a workflow republished five times before the moment arrives has five runs waiting. A time already in the past never fires. The step warns you rather than blocking the publish: **"The timestamp is set in the past, so it will never fire."** ### Record Created or Updated Starts a run when a contact or object record is created, or updated into the criteria you set. **Inputs** * Record type: the sentence reads **When a … is created or updated**. The picker lists your **Contacts** and **Objects** [schemas](/schemas). * **In segment** (contact schemas only): a [segment](/segments), or **All \** to write your own condition instead. * **Condition** (only when no segment is selected): describe what narrows the trigger, for example *"Only when the status becomes active"*. Leave it empty to start on every create and update. * **Name** and **Note for maxinja**. **Outputs** The triggering record, under **Name**. It is a live reference, so later steps read it fresh rather than working from a copy taken at trigger time. **Notes** Records have to *enter* the criteria. On creation, the run starts if the record already matches. On an update, it starts only if the record did not match before and matches now. Saving a record that already matched changes nothing, so this trigger does not fire again and again for the same contact. Picking a segment replaces the condition: membership becomes the condition, and the **Condition** field disappears. With no segment and no condition, this fires on every create and every update of that record type, including bulk tag, subscription and topic changes and unsubscribes. One bulk import becomes one run per record, and every step of every run can add AI, tool and email usage. See [Workflow billing](/workflows/billing). The condition is compiled once when you write it, not on every run. See [AI in workflows](/workflows/ai). ### Record Deleted Starts a run when a contact or object record is deleted. **Inputs** * Record type: the sentence reads **When a … is deleted**, over your **Contacts** and **Objects** schemas. * **In segment** (contact schemas only): a segment, or **All \**. * **Condition** (only when no segment is selected): checked against the record's last data before it was deleted, for example *"Only when the status was active"*. * **Name** and **Note for maxinja**. **Outputs** The deleted record's last data, under **Name**. Because the record is gone, this is a plain snapshot of values rather than a live record. It does not re-read the record or expand [evaluated attributes](/evaluated-attributes) and background [Scheduled evaluated attributes](/attributes#computed-attributes). **Notes** Every matching deletion starts a run. There is no entering rule here, unlike **Record Created or Updated**. ### Event Fired Starts a run from the [event](/events) you choose. The selector can bind a workspace event schema or a defined collected event. These have different configuration and publication behavior. **Inputs** * Event type: **When a … fires** selects a workspace event schema or a [collected event](/events#collected-events). * For a collected event, inspect its source, definition and revision before selecting it. * **Condition** (workspace event schemas): optionally narrows the event data, for example *"Only when the order total is over $100"*. A collected event uses its selected definition instead of this generated condition. * **Name** and **Note for maxinja**. A new step already suggests `event`. **Outputs** The fired event, under **Name**. A collected event carries the matched payload and its customer or object identity; the runtime keeps that subject attached to the run for later history decisions. **Notes** There is no segment option here. Segments filter contacts, and this trigger watches events. Workspace events can arrive from the API, integrations or a Fire Event step. Collected events arrive through their configured source and definition. A collected-event trigger requires an enabled definition and pins its revision at publication. Later definition edits do not silently rewrite that published trigger; review and republish to adopt the change. Publication establishes a source frontier, so this is not a backfill of historical matches. Currently a workflow can have up to 10 collected-event triggers and a workspace up to 50 active collected subscriptions. The identified customer or object must still exist in the same workspace and schema when the collected event is delivered to the workflow. Durable delivery deduplicates the same receipt, while separate real events can still start separate runs. ### Email Event Starts a run on email activity anywhere in your space. **Inputs** * **Email events**: pick one or more of `email.scheduled`, `email.sent`, `email.delivered`, `email.delivery_delayed`, `email.opened`, `email.clicked`, `email.bounced`, `email.rejected`, `email.sending_failed`, `email.complained`, `email.unsubscribed`. * **Recipient contact schema** (optional): the contact schema whose members you want to react to, or **No contact matching**. * **Condition** (optional): narrows it further, reading the recipient contact, the sent email, and the event, for example *"Only opens from a mobile device"*. * **Name** and **Note for maxinja**, as a single base name. **Outputs** Three values built from one base name. With the base `welcome` they are `welcome_contact`, `welcome_email` and `welcome_event`. With no base set, they are `contact`, `email` and `event`. The contact value exists only when a **Recipient contact schema** is set. **Notes** The step's own note is the important part: *"This trigger watches every email this workspace sends."* Emails from any workflow, broadcast or one-off send are in scope. To react only to the mail one particular step sent, use that step's **Email events** branch instead. See **Send Email**. **Recipient contact schema** filters, it does not just add context. Set it and the trigger fires only for emails whose recipient is a contact in that schema. Leave it empty and it fires only for emails sent to plain addresses with no contact behind them. Select no event types and the workflow still publishes, with a warning, but the trigger can never fire. Pick at least one. ### Evaluated attribute threshold Starts when a **Scheduled** [evaluated attribute](/evaluated-attributes) publishes a value that crosses a typed comparison. For example, Customer's `completedOrders30Days` changes from `"1"` to `"2"`, entering your **At least** `2` condition. An **On demand** attribute is not eligible for this trigger. **Inputs** * **Evaluated attribute**: choose an enabled Scheduled attribute with an available Event source. Unsupported types and unavailable sources are disabled. **Manage attributes** opens its owning schema's definitions. * **Comparison**: numbers and timestamps support **Greater than**, **At least**, **Less than**, **At most**, **Equals** and **Does not equal**. Strings and booleans use the last two. * **Threshold value**: enter an exact decimal for a number, a timestamp with a time zone for date/time, text for a string, or **True / False** for a boolean. Decimal values remain exact strings, such as `"100.50"`. * **Crossing direction**: **Enter the condition**, **Exit the condition**, or **Enter or exit**. Direction refers to matching the comparison, not merely a number increasing or decreasing. **Outputs** A fixed `threshold` value carries: | Field | Evidence it retains | | ------------------------------- | ---------------------------------------------------------------- | | `subject` | Contact or object type, record ID and owning schema ID. | | `attribute` | Attribute ID, key and the generation that produced the crossing. | | `dimensionKey` | The calculation dimension whose value changed. | | `previousValue`, `currentValue` | Exact strings, booleans or null values from the comparison. | | `direction` | `enter` or `exit` for that specific transition. | | `occurredAt` | When the crossing was recorded. | The output key is always `threshold`; this trigger does not offer **Name** or a generated condition box. Later steps use this frozen evidence. Load the identified contact separately when you need its email, and do not substitute today's attribute value when explaining why enrollment occurred. **Notes** Publication pins the attribute revision and establishes a baseline. Customers who already match do not all trigger immediately. Later complete published calculations produce crossings, including a change caused by a rolling window expiring. The calculated value can change when time passes without anyone editing the contact. With **At least** `2` and **Enter the condition**, `1 → 2` qualifies while `2 → 3` does not: the customer already matched. **Exit the condition** captures `2 → 1`. **Enter or exit** captures both transitions. Choose the direction that matches the message's purpose. A requested refresh is asynchronous. Reading published values or opening a preview does not start a refresh. Inspect status and the calculation's data window if a crossing seems missing; an incomplete refresh is not a complete new generation. To message the people who already match at launch, make that a separate, deliberately reviewed audience. Do not expect the threshold trigger to backfill them. ### Incoming Webhook Starts a run when an outside system posts JSON to this step's own URL. **Inputs** * **Trigger URL**: read-only. Use the generate button to create one, then copy it. The button becomes **Renew URL** once a URL exists. * **Expected payload**: the shape of the JSON body you will send. Paste a sample request and let maxclicks work the shape out, build it by hand, or generate it with AI. * **Name** and **Note for maxinja**. **Outputs** The parsed request body, under **Name**, after it has been checked against **Expected payload**. **Notes** The sending system posts an HTTP `POST` with a JSON body and an `Authorization: Bearer` header carrying an [API key](/api/authentication) for this space. The URL only goes live once you publish. Requests whose body does not match **Expected payload** are rejected, so later steps can rely on the shape. The workflow will not publish without a payload shape set. Treat the trigger URL like a secret. Renewing it generates a new one that only takes effect at the next publish: until then, outside services keep firing the currently published URL and the new one is not live yet. Duplicating a workflow gives the copy its own URL, so a copy never steals the original's traffic. ## Enhancers An enhancer adds a value to the run and hands it on. Each has a single **Then** output. ### Find Records Looks contact or object records up mid-run and puts them on the run. **Inputs** * Record type: the sentence reads **Find … records**, over your **Contacts** and **Objects** schemas. * **In segment** (contact schemas only): a segment, or **All \**. A live count of matching records appears underneath. * **Filter** (only when no segment is selected): describe which records to bring in, for example *"Customers who purchased in the last 30 days"*. * **Process each record separately**: see the note below. * **Name** and **Note for maxinja**. **Outputs** The matching records, under **Name**. Checked, later steps see one record at a time. Unchecked, they see the whole list as a single value. **Notes** Picking a segment replaces the filter and keeps following that segment's own definition, so the **Filter** field disappears. With neither a segment nor a filter, this brings in every record of the type, every run. The editor warns rather than blocking: **"Filter is not specified, which may impact performance."** Scope it deliberately, especially with **Process each record separately** checked, where a schema of 50,000 contacts becomes 50,000 runs. Checked and nothing matched, the run ends at this step. Unchecked and nothing matched, the run carries on with an empty list. Leave it unchecked when a later step has to act on all the records at once. A single run can carry at most **1,000 found records** as a list. For a larger selection, narrow the filter or enable **Process each record separately**. The matched selection is saved for this step; recovery continues through that selection rather than searching again and silently changing its members. ### Set Value Works a value out from what the run already carries. **Inputs** * **Value**: describe what to compute, for example *"The number of days since the contact signed up"*. Reshaping, math, text building, date arithmetic, assembling lists. * **Value shape**: the JSON shape of the result, so later steps know what they are getting. * **Name** and **Note for maxinja**. **Outputs** The computed value, under **Name**, in the shape you declared. **Notes** The saved expression calculates from its configured input; it does not run a separate query. Loading record values and selected attribute expansions can still read workspace data. Use **Query** or **Find Records** when you need an additional lookup. **Name** is required here: the workflow will not publish without it. ### Query Answers a factual question from your workspace data and passes the result to later steps. After an onboarding delay, Query can count projects created by this person so you do not send a reminder for work they have already done. **Inputs** * **Query**: describe the answer you want, for example *"Count projects whose creator is this run’s user; exclude projects created by their teammates."* A lookup, a count, an aggregate, or a deep read across records, events and email activity, optionally parameterized by what the run carries. * **Result shape**: the JSON shape of the answer. * **Name** and **Note for maxinja**. **Outputs** The answer, under **Name**, in the shape you declared. It is checked against that shape when the step runs. **Notes** This is the way to bring in data the run does not have yet. Where **Set Value** reshapes what is already there, Query goes and reads. Compile the query and inspect its explanation, **Depends on** and **Reads**. Check the relationship it follows as carefully as the count: account-wide activity and the individual’s activity answer different marketing questions. Place the query after a delay when the decision needs records as they stand at that point. A query failure is not the same as a valid zero result. ### AI Interprets customer context and produces an answer that later steps can reuse. For example, after Query confirms that a signup has no projects, AI can suggest a first project from that person’s stated goal. It can also classify supplied text or prepare a structured decision. Use **Query** for counts and lookups, and compiled **If/Else** for measurable rules. Use **AI** when the task needs interpretation. If only a passage in an email needs rewriting, [Adaptyle](/adaptyle) can handle that inside the email instead. **Inputs** * **Expected data**: which of the incoming values this step reads, and which related records to load with them. * **Prompt**: the step's standing instruction, for example *"Suggest one first project using this person’s role and stated goal. Return a short generic suggestion if those facts are missing."* This is not a message to maxinja: it runs on every run. * **Result shape**: the JSON shape the answer must follow. * **AI settings**: * **Model**: required. Pick the model that runs this step. * **Thinking**: required. **Off**, **Low**, **Medium** or **High**. Deeper thinking gives better judgment and makes every run slower and pricier. * **Web search**: requests a provider web-search capability. The current runtime refuses provider web search before inference because it has no supported execution budget. Leave this unchecked for an executable step. * **Workspace data access**: read-only query tools over your workspace's data. * **MCP servers**: appears when your space has servers registered. * **Name** and **Note for maxinja**. **Outputs** The model's answer, under **Name**, in the shape you declared. It is checked against that shape before the run continues. This answer belongs to the run. To save a recommendation or classification on the customer for later campaigns, connect **Create or Update Records** and explicitly map the result to a stored attribute on that contact. Keep the matching customer identifier in that action so the result updates the intended record. Review both structure and meaning. A result can match its shape while making a poor recommendation. Give the prompt the facts it may use, the required result and how it should handle missing context. Do not use AI to invent the data a previous lookup could not obtain. **Notes** This enhancer calls a model on every run. **Workspace data access** adds read-only query tools and workspace descriptions; MCP grants can add several model turns and external actions. **Web search** is currently refused by the runtime before inference. Inspect [tool grants and recovery](/workflows/ai#review-mcp-changes-and-uncertain-outcomes) before publishing, and review [Workflow billing](/workflows/billing). **Model** and **Thinking** are pinned on the step itself, with no workspace default behind them. Editing your workspace's AI settings can never change what a published workflow does. ### For Each Splits the run over a list the run already carries. **Inputs** * **Data path**: the list to loop over. * **Item type** or **Item shape**: read-only, worked out from the path. * **Expected data**: read-only, the one value the path enters. * **Name** and **Note for maxinja**. **Outputs** One item, under **Name**, per run. Reusing the list's own name is fine: the item takes the list's place for everything downstream. **Notes** This step always fans out, with no checkbox to turn it off. Each item continues the workflow as its own independent run, and an empty list ends the run right here. The list is limited to **1,000 items** and is captured before child runs start. For a larger record audience, use **Find Records → Process each record separately** instead. Later changes to the original list do not replace items already captured for this step. Point **Data path** at something that is not a list and the editor only warns you, but the run fails when it gets there. Check the **Item type** or **Item shape** line: if neither appears, the path is not a list. ## Controllers A controller routes or paces the run. None of them add anything to the run: whatever came in carries straight on. ### If/Else Sends the run down one of two paths. **Inputs** * **Condition**: describe when a run should take the **If true** branch, for example *"The order total is over $50"*, or a judgment call like *"this product fits the customer's interests"*. Anything else takes **If false**. * **AI on every run** (**Model**, **Thinking**): appears only while the condition is uncompiled. **Outputs** Nothing. The run carries its values on unchanged, down whichever branch it took. **Notes** The two outputs are **If true** and **If false**. Leaving one unconnected is legitimate: a run that takes it ends there. Ordinary condition logic evaluates the configured incoming data. Put a **Query** or **Find Records** step before it when more record context is needed. The dedicated Customer event history mode below reads a bounded event collection for the run's subject. Leave the condition uncompiled and an AI model weighs it on every single run, which adds latency and cost to each one. maxclicks warns you at publish and makes you pin a **Model** and **Thinking** depth on the step first. Compiling avoids model interpretation on each run. Standard internal execution has no fixed step fee; the result still depends on the supplied data. See [AI in workflows](/workflows/ai). #### Customer event history If the branch is about a customer's recorded activity, choose **Customer event history** in If/Else instead of describing a general condition. Select the event collection and configure: | Control | Decision it defines | | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | **Condition** | **Has occurred** or **Has not occurred**. | | **Look back (days)** | The 1–30-day interval ending when this step captures its decision window. | | **Allow late events (seconds)** | How long to allow events to arrive after that window closes; absence requires at least 60 seconds, with up to one day supported. | | **Match** | This customer/object, or the same correlation ID as the triggering event. | The run must identify exactly one customer or object. Correlation matching also needs a correlation ID on its trigger. The runtime saves the time window and identity evidence, waits if event storage is behind, and reuses the accepted result during recovery. It does not turn an unavailable history service into a false "has not occurred" decision. For example, after a signup wait, check whether this customer created a project in the relevant window. For an order-specific follow-up, correlation matching can distinguish this purchase from another purchase by the same customer. See [event-history decisions](/workflows/ai#decide-from-customer-event-history). ### Switch Sends the run down exactly one of any number of named paths. **Inputs** * **Case selection**: describe how to pick one of the cases below, for example *"Route by the order's plan tier: free, pro, or enterprise"*. * **Cases**: one row per outgoing path. **Add case** appends one, and each row has **Move up**, **Move down** and **Remove case**. Every case needs a value, and no two can be the same. * **AI on every run** (**Model**, **Thinking**): appears only while the selection is uncompiled. **Outputs** Nothing. The run carries its values on unchanged, down the case that was picked. **Notes** There is no built-in otherwise path. The selection always lands on a case by its value, and a selection that matches none of your cases fails the run. Make the cases exhaustive, or add an explicit catch-all case (something like `other`) and mention it in **Case selection**. Removing a case removes its path and the link going out of it. Reordering cases is cosmetic, but adding, removing or renaming one changes what the selection has to land on, so you will need to compile the selection again. At least two cases are needed to publish. ### Delay Holds the run and releases it later. **Inputs** * **Delay**: describe the wait, for example *"Wait 3 days"* or *"until 9am in the contact's local time zone"*. A fixed duration, a target moment, or one worked out per run from what the run carries. * **AI on every run** (**Model**, **Thinking**): appears only while the delay is uncompiled. **Outputs** Nothing. The run carries its values on unchanged. **Notes** There is no duration box and no date picker. You describe the wait, and whether it comes out as a duration or as a moment follows from your description. Delays are durable. The duration or target time is evaluated once and the deadline is saved, so a restart does not repeat the interpretation or reset the wait. Pausing the workflow parks the run but keeps that deadline; if it passes during the pause, the run can continue after resume. A wait that depends on data the run does not carry needs a **Query** or **Find Records** step before it. ## Actions An action changes something, inside maxclicks or outside it. Each has a single **Then** output, except **Send Email**, which has two. ### Create or Update Records Builds contact or object records out of the run and writes them. **Inputs** * Record type: the sentence reads **Create or update … records**, over your **Contacts** and **Objects** schemas. * **Records**: describe the records to write, for example *"a contact from the event payload email and name"*. One record or a whole list. * **Records shape**: read-only, telling you whether your description produces one record or a batch. * **Process each record separately**: appears when the description produces a list. * **Name** and **Note for maxinja**. **Outputs** The records as they now stand after the write, under **Name**. These are the stored records, so they carry everything, not just the fields you set. **Notes** Existing records are matched and updated rather than duplicated. Contacts are matched by their id, then `userId`, then email, then phone. Objects are matched by their id, then external id. Anything matching nothing is created fresh. An update merges. Fields your description does not mention keep the values they already had. Contacts this step creates are subscribed to every communication channel unless your description sets their subscriptions explicitly. If these contacts did not knowingly opt in, say so in the **Records** description, or you are adding people to your marketing audience without their consent. See [Contacts](/contacts). Writing records sets off **Record Created or Updated** triggers and [webhooks](/webhooks) elsewhere in your space, including in this same workflow. Creating contacts also changes the contact count your plan is billed on. With **Process each record separately** checked and nothing written, the run ends at this step. ### Delete Records Permanently deletes contact or object records. **Inputs** * Record type: the sentence reads **Delete … records**, over your **Contacts** and **Objects** schemas. * **In segment** (contact schemas only): a segment, or **All \**. * **Filter** (only when no segment is selected): describe which records to delete, for example *"contacts unsubscribed over a year ago"*. * **Process each record separately**: required here, so set it deliberately. * **Name** and **Note for maxinja**. **Outputs** Deletion results under **Name**: each contains the record's `id` and `deleted: true`. With **Process each record separately** checked, each result starts its own continuation; otherwise the results form one list. The results do not contain the deleted record's old fields. **Notes** Deletion is permanent, with no undo or recycle bin. Execution passes deletion markers to later steps, not the old record content. Do not build a later message or external request that depends on recovering fields from that result. With neither a segment nor a filter, this targets **every record of the chosen type** on each run. maxclicks only warns you at publish: **"No filter is specified, so this step permanently deletes every record of the selected type."** It does not stop you. A segment ignores what the run carries. It deletes every contact in that segment each time any run reaches the step, not just the contact this run is about. An execution can delete at most **1,000 records**. A larger match fails before the deletion is requested; narrow the criteria. After requesting deletion, the step waits for that operation to complete before continuing. Check its status before attempting the work again. Deleting sets off **Record Deleted** triggers and webhooks, and deleting contacts changes the contact count your plan is billed on. ### Fire Event Fires one event out of the run. **Inputs** * Event type: the sentence reads **Fire a … event**, over your event schemas. * **Event data**: describe what the event carries, for example *"a purchase-completed event with the order total"*, including who or what it belongs to. * **Name** and **Note for maxinja**. **Outputs** The fired event, under **Name**, as it was stored. **Notes** One event per run, never a batch. Events are immutable: nothing this step does ever updates an existing one. The event lands on its record's timeline, and it sets off **Event Fired** triggers and [webhooks](/webhooks). This is the supported way to chain one workflow into another, since a workflow cannot loop back on itself. The step continues only when the accepted event is **ready**. It waits while event preparation is pending and fails if preparation cannot complete. An accepted event is not yet evidence that another workflow ran or an outbound webhook was delivered. Any [attribute](/attributes) with auto-fill turned on is filled in for fields your description leaves out, and an AI-backed auto-fill spends credits each time. ### HTTP Request Calls an outside endpoint and adds the response to the run. **Inputs** * **Expected data**: which of the incoming values this request reads, and which related records to load with them. The templates below interpolate it. * **Method**: **GET**, **POST**, **PUT** or **DELETE**. **GET** hides the body, content type and signature fields. * **URL**: required. Supports [Liquid](/liquid), so `{{ }}` drops in values from earlier steps. Use the `{ }` button rather than typing a reference from memory. * **Headers** (optional): edit as rows, or switch to JSON for headers that only exist under a condition. * **Content type** (optional): sent as the `Content-Type` header. * **Body** (optional, not on **GET**): what the request sends, with Liquid in its text values. * **Body signature** (optional, not on **GET**): generate a secret and maxclicks signs each request so your endpoint can verify it really came from you. * **Response shape** (optional): the JSON shape the response must follow. * **Name** and **Note for maxinja**. **Outputs** Under **Name**: the parsed response body under `data`, alongside `status`, `statusText` and `headers`. **Notes** Only a 2xx response counts as success. The response must be JSON and, when supplied, match **Response shape**. The request has a 30-second timeout, a 1 MiB request and response limit, and does not follow redirects. The request includes an `Idempotency-Key` for this run and step occurrence; the receiving service should honor it. A recorded successful response can be reused during safe infrastructure recovery. A timeout or uncertain result is not blindly replayed. Check the receiving system before re-triggering: it may have completed the action even though maxclicks could not validate the response. Rotating the signing secret only takes effect at the next publish. Until then this step keeps signing with the currently published one, so teach the receiving endpoint the new secret before you publish, not after. To verify a signature, hash the raw body exactly as it arrived. Parsing the JSON and re-serializing it changes the bytes and breaks the check. Requests can only reach public addresses on the internet, so this step cannot be pointed at a private or internal network. ### Send Email Sends this step's own email to one recipient. **Inputs** * **Sends to**: required. One contact, or one email address, taken from what the run carries. A list of contacts cannot be one recipient: put a **For Each** step in front, or set **Find Records** to process each record separately, so each one gets its own email. * **Email template**: **Edit** opens the full email editor. Before a template exists you pick a starting point (**Dynamic Composer**, **Plain Composer**, **MJML Code** or **HTML Code**) and a **Sending mode**: * **Marketing**: *"Only subscribed contacts; manages the unsubscribe link."* * **Transactional**: *"All contacts regardless of subscription; no unsubscribe link."* * **Email event branch** (optional): the email event types the second output should fire on. * **Name** and **Note for maxinja**: appears once you select event types. **Outputs** * **Then** adds nothing. The run carries on when the email provider accepts the send. Delivery, opens and clicks happen later. * **Email events** adds three values from one base name: `_contact`, `_email` and `_event`. With no base set they are `contact`, `email` and `event`. The contact value exists only when the email went to a contact rather than a bare address. **Notes** Consent is enforced, and a send that breaks it fails the run rather than going out anyway. Marketing content can never go to a bare email address, because there is no contact behind it, no consent, and no working unsubscribe link. Marketing content to a contact requires that contact to be subscribed to email, and to hold the communication topic when the template names one. Any suppression (unsubscribed, bounced, complained, manual or legal) blocks marketing content, and a bounce suppression blocks transactional content too. **Sending mode** cannot be changed after the template is created, so pick it deliberately. [Adaptyle](/adaptyle) adds model usage per recipient when instructions remain after the message's conditions and Liquid are resolved. The finished message also draws on your plan's email allowance. See [Workflow billing](/workflows/billing). The **Email events** branch does not resume this run. Each event starts a new run entering this step on that second output, so it shows up separately on the Runs tab. Subsequent AI, metered tools and email sends add their own usage; there is no fixed internal run fee. Leave the event types empty and the branch never fires at all, and connecting it while empty blocks the publish. The template belongs to this step alone, so editing it changes nothing else in your workspace. Publishing takes a copy of it, which is why runs already in flight on an older version keep sending the older email. ## Decorators ### Note Documents the canvas for whoever opens this workflow next. **Inputs** * **Content**: the note itself, in Markdown. * **Description**: an optional line describing what the note is about, which teammates and AI agents read first. * **Color**: one of the badge colors. New notes start yellow. * **Last edited by**: read-only, showing who saved it last. Drag the bottom-right corner on the canvas to resize it. **Outputs** Nothing. A note has no connection points at all, so nothing can link into or out of it, and it takes no part in any run. **Notes** Editing a note still marks the workflow as having unpublished changes, even though nothing about a run has changed. ## Placeholders ### None An empty step holding its place on the canvas until you give it a type. **Inputs and outputs** None. **Notes** You never pick this from the step type list. maxclicks creates one when you add a step from an outgoing connection point, and you then choose what it should be in the side bar. A workflow still holding one will not publish: **"This is a temporary step type, resolve it before publishing the workflow."** ## Processing many records Three steps carry a **Process each record separately** checkbox: **Find Records**, **Create or Update Records** and **Delete Records**. | Setting | Behavior | | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | Checked | The run fans out: one independent run per record, and later steps see a single record at a time. If the step produced no records, the run ends there. | | Unchecked | The run stays one run, carrying every record together as one list, and it continues even when the step produced nothing. | **For Each** always fans out, one run per item, with no checkbox. Fan-out is the biggest multiplier on what a workflow costs. Every run charges for every step it executes, so a step that fans out over 5,000 records turns one run into 5,000. Checking or unchecking the box also changes the shape of what later steps receive (one record, or a list), so steps after it may need reconfiguring. ## Related * [Workflows](/workflows): the canvas, runs, versioning, and the trigger API. * [Create a workflow](/workflows/create): build the graph step by step. * [Manage workflows](/workflows/manage): publishing, pausing, and versions. * [Workflow runs](/workflows/run): see what a step did, and why a run failed. * [AI in workflows](/workflows/ai): compiled logic, logic worked out per run, and the AI step. * [Workflow billing](/workflows/billing): what each step costs, and the concurrent run cap. * [Segments](/segments): reusable contact filters for triggers, Find Records and Delete Records. * [Evaluated attributes](/evaluated-attributes): on-demand customer facts and related-record lookups. * [Scheduled evaluated attributes](/attributes#computed-attributes): Scheduled evaluations used by the threshold trigger and step logic. * [Templates](/templates): email content for Send Email. --- # Create a workflow Source: https://docs.maxclicks.ai/workflows/create # Create a workflow Turn the journey brief into a workflow your team can review. Ask maxinja to build it or add and configure steps on the canvas, then check the trigger, conditions, timing and email before publishing. The [workflow overview](/workflows) explains how those parts work together. ## Start a workflow ### Open the editor Open **Workflows** in the navigation bar at the top, then **New workflow**. An empty canvas opens. ### Or copy an existing one Open the **⋯** menu next to a workflow and choose **Duplicate**. The copy is an unpublished draft. Canvas controls: scroll or drag empty space to pan, use the bottom-left controls to zoom, and hold **Command** while dragging a step to snap it to the grid. ## Add steps A workflow must start with a trigger step. Add one first, then add the steps that run after it. * **Add**: hover a step and click the **+** that appears. Pick a type from the list. The new step is linked to the one you added it from. * **Delete**: select a step and press **Backspace**. Hold **Shift** to select several, then delete them together. Each run carries its own data. The trigger puts the first value there, and every step after it can read what earlier steps added. See the [step list](/workflows#steps) for what each type needs and what it adds. ## Connect slots to steps Steps connect through [slots and links](/workflows#slots-and-links). * A **slot** is an output point on a step. A Switch or If/Else step has one slot per branch. * A **link** connects one slot to one target step. A slot has at most one outgoing link. * **Connect**: drag from a slot's connection point to the target step. * **Disconnect**: select the link and press **Backspace**. ## Configure a step Select a step to open its panel. You never hand-write conditions, values, queries, or audiences: you describe what you want in plain language, maxinja builds the logic, and it shows you a readable summary to confirm or refine. See [step logic](/workflows#steps). ## Build progressively Steps can sit half-configured while you work. The editor displays issues as you configure them, and publication performs the final graph validation. Lay out the flow, then resolve the data and behavior on each step. To leave an explanation for your team, add a Note step: it sits on the canvas and takes no part in a run. ## Review the journey as an operator Trace one realistic customer through the canvas. Confirm the trigger represents the intended milestone, the records belong to that customer, each branch has the data it needs and every Send Email has one eligible recipient. Preview the step-owned email using **Data**, **Personalize** and **Checks** in the composer rail. Check every fan-out control and list size. If a step finds all contacts and processes each separately, later steps run once per result. Decide how an empty result should behave. For HTTP Request, agree an idempotent receiving contract and a valid response shape before letting the journey act on another system. For absence-of-event logic, use the explicit event-history window and lateness controls. For Evaluated attribute threshold, remember publication establishes a baseline instead of immediately triggering existing matches. The [block library](/workflows/blocks) explains these differences. Check execution with a narrowly scoped test copy before broad enrollment. The [controlled-run walkthrough](/guides/workflows#verify-a-controlled-run) shows how to verify the lookup and both branches without the customer-facing email action. A composer preview does not execute the earlier workflow steps. ## Validate at publish Publishing checks the whole workflow and stops on any error. Clear each message to publish. Errors about the workflow as a whole: | What is wrong | Message | | ----------------------------------------------------------- | ------------------------------------------------- | | No trigger step | `A workflow must have at least one trigger step.` | | A trigger has an incoming link | `Trigger steps must not have incoming links.` | | A step that is not a trigger or a Note has no incoming link | `This step must have at least one incoming link.` | | A step loops back to an earlier one | `Currently, cycles are not supported.` | Individual steps flag their own gaps too, for example `Schema is not specified.` or `The condition requirements need to be converted to a predicate.` Rewiring the canvas so a step no longer receives a value it was set up to use also flags that step. ## Publish Click **Publish** at the top. The button appears only while the workflow has unpublished changes. Publishing takes a frozen copy of the workflow, called a version, and makes it the live one. It also resumes a paused workflow. * New triggers start runs on the latest published version. * Runs already underway finish on the version they started on. * Editing after publishing saves a draft. The changes take effect at the next publish. Use **Live / Paused** to prevent new runs and park existing execution. It does not reverse completed effects, and publishing unpauses the workflow. Inspect old runs before publishing a fix because they retain their original version. ## Related * [Workflows](/workflows): steps, slots and links, versioning, and the trigger API. * [Workflow blocks](/workflows/blocks): every step type, explained. * [Manage workflows](/workflows/manage): publishing, pausing, and versions. * [Workflow runs](/workflows/run): what happened on each run. --- # Manage workflows Source: https://docs.maxclicks.ai/workflows/manage # Manage workflows Publishing saves the journey that new qualifying customers follow. Later edits stay in its draft until you publish another version. Existing runs retain the version they started, including its steps and step-owned emails. You need workspace write permission to publish, stop enrollment, pause, resume, duplicate, delete or terminate a run. Choose the control by the work you intend to stop: | Control | New enrollment | Existing execution | History and completed effects | | ------------------------------ | -------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------- | | **Stop new runs** | Stops new trigger enrollment. | Existing runs continue. | Retained. | | **Live / Paused** or **Pause** | Paused workflows do not enroll new activity. | Parks further step execution. | Retained; completed effects remain. | | **Terminate run** in **Runs** | Does not stop the workflow's other triggers. | Ends the selected run, its children and pending continuations. | Retains evidence of accepted effects. | | **Delete** | Removes an eligible workflow. | Deletion is refused when run history or accepted work exists. | Use only for eligible unused configuration. | For example, stop new runs when retiring an old reminder while allowing already-enrolled customers to finish. Pause when the current email or external action needs investigation. Terminate affected runs if continuing their original version would be wrong. ## Publish it to make it live The **Publish** button appears at the top of the editor whenever you have changes that have not been published. maxclicks checks the whole workflow first, then asks you to confirm: publishing applies your changes to all new triggers. If anything is wrong you get **Cannot Publish**, and the problems are listed on the canvas and inside the offending steps. The usual ones: | Message | What it means | | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | A workflow must have at least one trigger step. | Nothing would ever set it off. | | This step must have at least one incoming link. | The step is stranded. Connect it, or delete it. | | The condition is not compiled yet. | You described the logic but never compiled it. Compile it, or on an If/Else, Switch or Delay accept that maxinja will work it out on every run. | | This is a temporary step type, resolve it before publishing the workflow. | A **None** step is still holding a place. Give it a type. | | Currently, cycles are not supported. | Two steps loop back on each other. Break the loop. | Warnings do not block a publish. **The scheduled timestamp is set in the past** and **Without a data key, next steps will have no access to the found records** are worth reading before you go ahead, not after. You also need something to publish. If nothing has changed since last time, the publish is refused. Publish from the editor, or have maxinja perform the authorized workflow action after your review. The public workflows API does not expose publication. ## Keep editing while it runs You edit a workflow in place, and steps can sit half finished while you work. None of it touches the live version. Your edits go live only when you publish again, and until then the workflow carries a **Draft** badge alongside its live status. Publishing takes a frozen copy of the steps, connections and step-owned email templates, called a version. Later draft edits do not alter that copy. Publishing also replaces the live trigger configuration and resumes a paused workflow. A separate **Stop new runs** setting remains in place when you publish; publication does not clear it. * New triggers always start on the newest published version. * Runs already going finish on the version they started with, however many times you publish in the meantime. * Every step is checked once more just before it executes, so a step that has become invalid fails that run rather than doing something unintended. This is why the canvas in the [Runs](/workflows/run) tab can look different from the one you are editing: it shows the workflow as it was when that run began. ## Pause it without losing anything Pausing prevents new runs and parks existing runs before they execute more steps. It does not undo completed writes or recall a request or email already in flight. Use it to contain a problem while you inspect the affected runs. A Delay keeps its saved deadline while paused. Resuming does not start the wait again; a deadline that has passed can continue once execution resumes. To end a particular journey permanently, use **Terminate run** in **Runs**. There is no skip or rewind control. Existing runs still belong to their original published version. If that version contains a faulty step, publishing a correction does not repair those runs: publication resumes the workflow, including work waiting on older versions. Review that consequence before publishing a fix. In the editor, a published workflow shows a **Live** button at the top. Click it, confirm, and it turns into **Paused**. Click that to resume. You can do the same from the **⋯** menu on the workflows list, with **Pause** and **Resume**. Use the Runs tab to inspect the version and current step before resuming. A pause is an execution hold, not a new version or a replay of every event that occurred during the hold. If **Stop new runs** is also active, resuming execution keeps new enrollment stopped. ## Stop enrollment while customers finish In the published workflow's editor, click **Stop new runs**. The control changes to **Resume new runs**. Existing runs continue using their original versions, so this is useful when replacing a journey or completing its last enrolled cohort. Stopping enrollment does not recall email or undo records already written. Already accepted trigger work and setup-change entry history are separate durable work to inspect. Check **Runs** and any setup review status when retiring a workflow; a button click is not evidence that every earlier entry has finished. **Resume new runs** re-enables enrollment only when the workflow is also unpaused. Activity from a stopped interval is not a general historical backfill. If you need an initial cohort, review that audience and its enrollment separately. ## End an individual journey Open **Runs**, select the run and click **Terminate run** in its details. Confirm the action to end that run, child runs and pending continuations. Other customers' runs and the workflow's enrollment setting are unchanged. Completed external effects remain: termination cannot recall an accepted email, reverse an HTTP request or restore a deleted record. The history records an explicit termination at the current step, which can make the run count as failed. Inspect accepted effects before arranging replacement work. A run or child that is committing a step can temporarily block termination. Read the error and retry after that step settles. A very large descendant tree may require terminating child runs first. This is a stopping control, not replay-from-step recovery. ## Read the status badges The Status column on the workflows list carries up to two badges at once. | Badge | Meaning | | ------------- | --------------------------------------------------------------------------- | | **Published** | Live. New triggers start runs. | | **Paused** | Published, with new runs prevented and existing execution parked. | | **Draft** | Either never published, or published with unpublished edits sitting on top. | The list also shows the total number of runs each workflow has had, and you can sort by status or by runs. The **Published** badge alone does not show whether **Stop new runs** is active. Open the workflow and inspect its enrollment control as well as **Live / Paused**. **Draft** can appear beside either published status because edits and execution state are separate. ## Copy or delete one **Duplicate**, in the **⋯** menu, copies the current draft and its step-owned emails into a new unpublished workflow. It does not copy run history. If the original has unpublished edits, those edits are part of the copy; inspect it rather than assuming you copied an older live version. Use a narrow trigger on a test copy. Publishing two copies with overlapping enrollment can give one customer two journeys. An Incoming Webhook gets a separate URL in the copy. **Delete** permanently removes eligible workflow configuration. It is refused when the workflow has run history, retained entry history, setup holds, published collected-event triggers or accepted automation awaiting completion. Stop new runs and retain the workflow for history when it has been used. Resolve existing execution separately with pause or run termination. Deletion also cleans up eligible step-owned template previews and can fail if that cleanup or the configuration changes during the operation. Inspect the reported reason. Do not treat a rejected delete as a successfully retired workflow: check enrollment and execution controls explicitly. ## Release a correction deliberately For a repeat-purchase reminder with a wrong customer lookup: 1. Pause to contain further execution and inspect affected runs, their versions and accepted effects. 2. Correct the draft lookup and test customers who should continue and customers who should exit. 3. Terminate old runs that must not continue on the faulty version. A newly published lookup does not rewrite those runs. 4. Set **Stop new runs** if you want enrollment to remain closed while reviewing the release. 5. Publish the correction. Publication resumes execution, including surviving runs on older versions. 6. Inspect run history, then resume new enrollment when appropriate. A successful publication validates the graph and required configuration. It does not prove the customer identity, intended audience or external service behavior. Check a real controlled execution before broad enrollment. ## Manage it from your own code The API can list your workflows, read one, and pause or resume it. It cannot create, edit, publish or delete them, and it never returns the steps, the links between them, or the versions. See the [workflows API reference](/api/workflows). To start a run from your own code, see [triggering a workflow](/workflows#trigger-a-workflow). ## Related * [Create a workflow](/workflows/create): build the graph. * [Workflow runs](/workflows/run): inspect individual runs. * [Workflow billing](/workflows/billing): how runs consume credits. --- # Workflow runs Source: https://docs.maxclicks.ai/workflows/run # Workflow runs Run history shows what happened to an individual customer journey. Each trigger creates a run that follows the published workflow, recording the steps it reaches and where it completes or fails. Use that evidence to investigate an unexpected outcome. The Runs tab is where you go when an automation did not do what you expected. ## Open the Runs tab Open the workflow and choose **Runs**, next to **Composer** at the top. The tab only appears once the workflow has been published at least once, since there is nothing to run before that. A panel beside the canvas lists the most recent runs, newest first, each showing when it started and how it ended: | What you see | What it means | | ------------------- | ------------------------------------------------------------------------------------------------------------------------- | | **In progress** | The run has not completed. It may be executing, on a Delay, paused with its workflow, or waiting for credits or capacity. | | **Finished!** | It reached the end with nothing failing. | | **Failed.** | A step failed and the run stopped there. | | **Needs attention** | Execution has a recovery reason and does not continue automatically. Inspect the reason and accepted effects. | Use **Terminate run** in the selected run's details to end that run, its children and pending continuations. You need workspace write permission. To contain all journeys, pause the workflow; to stop enrollment while existing runs finish, use **Stop new runs**. Completed effects remain, and an external request already in flight can still complete. See [workflow controls](/workflows/manage). Select a run and the canvas redraws it: the steps it went through are outlined in green, the step it is sitting on right now in the accent colour, and a failed step in red. The connection points along the path it took are highlighted, so a branching workflow shows you at a glance which way this particular run went. The **Workflow run details** panel in the corner gives you when the version was published, when the run was triggered, how many steps it went through, and either its status or when it finished. ### Read all of the history History is paginated. **Visible history** identifies the step range currently loaded, and the graph highlights that page. **Earlier steps** loads preceding evidence; **Latest steps** returns to the newest page. A path outside the visible range is not evidence that those earlier steps never ran. **Steps** is the run's recorded execution count. **Emails in visible history** counts email records only in the loaded page. Once all history is visible, the label is **Emails created**. An email record count does not establish provider acceptance or delivery; open its activity. **Branched from** links a child to its parent and the parent step that split it. Use that relationship before counting apparent duplicates. A root with three matching records can deliberately create three children, each executing later steps separately. ## See what a step did Click any step on the canvas and its history opens underneath it: * **Entered at** and **Left at**, and how long it **Took**. * **Received** and **Produced**: the values the step was handed, and the values it added, each listed by name with a short summary. * For a Send Email step, the **Email status** and a **View email details** button, which opens the message itself along with its **Activity**: sent, delivered, opened, clicked, bounced, unsubscribed. * The failure reason, in red, if the run stopped there. Compare **Received** and **Produced** to the decision you expected. An empty lookup may be a valid result; a controller may pass existing values on without adding a new one. Trace the recipient and the relevant purchase, event or object back through the path before treating a missing output as a failure. ### Distinguish live values and saved evidence Publishing freezes configuration, not every future customer fact. Ordinary record references can be read when a later step needs them. On-demand evaluated attributes execute saved lookup logic. Scheduled evaluated attributes read complete published calculations, with generations that can be pinned for the execution. Query output describes the lookup made at that step. An accepted AI decision, event-history decision and loop input can be retained for recovery. The **Evaluated attribute threshold** trigger supplies the exact previous and current values that caused enrollment. Compare those snapshots with their time and generation; today's profile can differ without making the original crossing wrong. Source-record deletion can remove private run payloads and history while preserving run identity, lineage and financial receipts. Missing old values after that cleanup are not permission to reconstruct them from a newer profile or replay the old action. ## When a run fails A terminal step failure ends that run. There is no general replay-from-this-step control. Temporary waits and safe infrastructure retries are different from terminal failures: check the recorded reason before deciding that a customer was skipped. The failed step is outlined in red and shows the failure message right on the canvas, without needing to select it. Common ones: | Message | What to do | | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **The recipient email address is suppressed.** | The address bounced, complained or unsubscribed. Nothing to fix in the workflow. | | Insufficient credits while execution is waiting | Restore available credits. Waiting execution can continue; do not re-trigger it and create a second journey. A run already marked Failed needs investigation. | | An HTTP Request failure or Needs attention | Check URL, headers, payload, response shape and the receiving system. A timeout or response-validation failure may happen after the external system acted. Reconcile before triggering another call. | | No available emails, an unverified sender domain or disabled sending | Correct the sending prerequisite. Email allowance exhaustion is distinct from a credit wait. | | Email acceptance or AI/tool outcome is unknown | Inspect the recorded recovery reason and provider or external activity. The run does not blindly repeat uncertain work. | If the failure is in the workflow, fix the draft and publish. New triggers use that corrected version; existing runs keep their original one. Publishing also unpauses the workflow, so check older waiting runs before releasing them. ### Waits and safe recovery Long delays persist their wake-up time and survive restarts. Credit shortage and concurrency limits park work for another attempt rather than failing the run. Temporary service-capacity or explicitly retryable delivery responses can also wait. The runtime records completed step effects and charges so a safe recovery can reuse them. That does not guarantee every external action is safe to repeat. An HTTP call, email or AI/tool execution with an uncertain outcome can require attention rather than being blindly repeated. Automatic infrastructure retries are bounded; exhausting them is a reason to inspect the run. ### Inspect Needs attention The details show **Needs attention**, **Stopped at** and the recovery reason. The run does not continue automatically. Topping up credits, refreshing the page or publishing another version does not establish that the held action is safe to repeat. Check the action's destination: email activity for Send Email, the receiving system for HTTP Request, and tool or model evidence for AI. Find whether the action was accepted and whether it already changed anything. Retain that evidence with the run when deciding the next customer action. If the old journey should end, use **Terminate run**. There is no general skip, rewind or replay-from-this-step control. A replacement run is new execution: it can repeat writes, sends and paid generation. Define its scope from the remaining work rather than replaying the entire journey because an earlier result is unclear. ## Terminate a run Select the run and click **Terminate run** under **Run control**, then confirm. This ends the selected run, descendants and pending continuations. Other roots in the same workflow are unaffected, and new triggers can continue unless you stop enrollment or pause the workflow. An explicit termination at a current step is recorded as a failure reason. Accepted external effects and incurred charges remain. A termination of the parent includes children created by Find Records or For Each, so inspect the lineage before using it. If a step is committing, the server asks you to retry after it finishes. If one termination cannot settle the descendant tree, terminate child runs first and retry the parent. A rejected termination is not a completed stop; reopen the details and check its state. For a faulty reminder, first contain ongoing execution, then identify every root and child affected by the wrong lookup. Terminate the journeys that must not proceed on that version, correct the draft and verify a controlled example. Publishing the correction can resume surviving old runs, so review them before publication. ## Why one trigger produced several runs Two things split a run into many: * A **Find Records**, **Create or Update Records** or **Delete Records** step with **Process each record separately** checked. Each record carries on as its own independent run, with its own history. * A **For Each** step, which always splits, one run per item in the list. Each split is a real child run with its own subsequent step execution and cost. Child runs retain their parent relationship, which helps distinguish intentional fan-out from duplicate triggering. Count executed steps across the children when estimating usage, not only the number of boxes on the canvas. ## Which version a run is on Publishing takes a frozen copy of the workflow. A run stays on the copy it started with and finishes on it, even if you publish three more times in the meantime. Only new triggers pick up the latest version. That is why the canvas you see in the Runs tab can differ from the one in the Composer. It is showing the workflow as it was when that run began. See [Manage workflows](/workflows/manage). ## Measure across all the runs The **Metrics** tab answers the volume questions rather than the single-run ones. It shows the success rate, total runs, and how many completed, failed and are still waiting, all for one published version, which you pick from the **Versions** list in the same side panel. **Where the river leaks** ranks the steps losing the most runs. Click one and the canvas jumps to it. That is the shortest route from "this automation underperforms" to the step responsible. ## Read runs from your own code The API lists runs and retrieves a run's status, step count and paginated history, including parent information for child runs. It does not return the live values the run carries. See the [workflows API reference](/api/workflows). ## Related * [Workflows](/workflows): steps, triggers, and how a run flows. * [Manage workflows](/workflows/manage): publishing, pausing, versions. * [Workflow billing](/workflows/billing): what each run costs, and the cap on runs at once. --- # Your workspace Source: https://docs.maxclicks.ai/workspace # Your workspace A workspace keeps a business's customer data, brand, campaigns, journeys and team together. For an agency managing two clients, separate workspaces keep their contacts, credentials and bills apart. Use segments within one workspace when you only need different audiences for the same business. Almost everything on this page lives under **Settings**, which you open from the workspace menu at the top of the app (the one showing your workspace name). Admins also get **Members** and **Billing** shortcuts in that same menu. ## Name it and set its address Open **Settings**, then **General**. You can set three things: the workspace logo, the name, and the slug. The slug is the part of the address bar that identifies your workspace, so changing it changes every link into this workspace. It has to be unique across all of maxclicks, and the name has to be unique among the workspaces you belong to. Either field tells you it is not available before you can save. General is admin-only, along with Brand, Members, maxinja, Plans, Billing, and Forms Protection. Domains, Sender Profiles, Schemas, Events, Topics, and Developers are open to every member. **Name** and **Slug** save automatically after you stop typing. Wait for validation and the save to finish before sharing the new address. Changing the display name does not move customer data or change the workspace's billing identity. The workspace's data region is selected during setup and cannot be changed here. Create a separate workspace when you need a different region; switching workspaces does not transfer its records or campaigns. ## Get it ready to send A brand new workspace cannot send email yet. Two things have to happen first, and both are worth doing on day one: ### Verify a sending domain Under **Settings**, then **Domains**, add the domain you send from and follow the DNS steps. Sending stays off until one is verified, and the app nags you about it: **No Sending Domain** while you have none, **Unverified Domain** while one is waiting on DNS. See [deliverability](/domain) for the full walkthrough. ### Pick your defaults maxclicks pre-selects a default wherever a choice is needed, so you are not choosing a domain and a sender for every email. Open the row menu on **Domains**, **Sender Profiles**, or **Schemas** and choose **Set as default**. Your first contact schema becomes the default automatically, and a workspace always keeps at least one. Delete the default while you have others and maxclicks promotes one of them in its place. Try to delete the only one you have and maxclicks refuses: "Cannot delete the last contact schema". First domain verification enables sending from that domain and ends eligibility for platform-funded initial onboarding. It grants no AI credits. Eligible new workspaces can receive funded maxinja setup assistance beforehand; ordinary AI operations still need available credits. See [billing](/billing). ## Invite the people you work with **Settings**, then **Members**, then **Invite team members**. Everyone you add holds one of three roles (Admin, Member, or Non-paid Admin), and a workspace always keeps at least one person with administrative permission. Roles determine editing and administrative permissions. Collaborators and maxinja access have no per-seat charge; permission requirements still apply. See [members](/members) for access and membership changes. ## What your plan covers A workspace with an active subscription is Premium. **Settings → Plans** shows the monthly offer; new annual subscriptions are unavailable. [Billing](/billing) explains included capacity, recipient sends and separately paid AI work. ## Understand what a default changes Defaults reduce setup for the next item you create; they are not a promise to rewrite every existing message. Review the sender and brand on live templates, scheduled broadcasts and published workflow versions before changing workspace settings. Campaign revisions and published workflow email templates preserve the configuration used by that work. Use the workspace for one coherent customer model and operating team. Separate spaces have separate credentials, domains, data and balances; selecting another workspace does not move a contact or campaign there. Changing the default domain also selects the first sender belonging to that domain, or clears the default sender when it has none. Set the intended sender after changing the domain. A default sender must belong to the current default domain; a default contact schema must be a contact schema in this workspace. Changing field definitions and their dependent campaigns is a different operation. Use a [setup change](/setup-changes) when the attribute editor requires Review; changing a workspace default does not convert record values or repair dependent definitions. For example, an agency switching a client's default from `mail.example.com` to `updates.example.com` should check the new sender profile before creating its next campaign. An already prepared campaign keeps its captured sender configuration. ## Watch for notices maxclicks raises anything that needs your attention in an **Inbox** panel, opened from the icon in the top bar. You can filter it by **Critical**, **Action**, or **Updates**. What actually turns up there: * **Low Credits** and **Credits Depleted**, as available usage runs low. New work may be refused; broadcasts and workflows can wait for credits. * **Email Limit Reached** on Free, and **Monthly Email Limit Exceeded** once a Premium workspace passes its current paid sending allowance. * **No Sending Domain** and **Unverified Domain**, each linking straight to the page that fixes it. * Deliverability warnings raised against your sending reputation: **DKIM Setup Needed**, **SPF Setup Needed**, **DMARC Setup Needed**, **BIMI Setup Needed**, **Bounce Rate Warning**, **Complaint Rate Warning**, and **Sending IP Warming**. These are the ones worth acting on quickly, because they compound: see [deliverability](/domain). ## Issue an API key **Settings**, then **Developers**, then the **API keys** tab. A key works in this one workspace, carries its owner's access, and is shown to you exactly once. [Members](/members) covers who can issue a key and what to do when one leaks, and [authentication](/api/authentication) covers using it against the API. ## Working in more than one workspace Your account can belong to several workspaces. The menu at the top of the app lists the ones you are a member of, switches between them, and offers **New workspace**. Nothing is shared between workspaces: contacts, schemas, domains, credits, and billing are all separate. ## Deleting a workspace **Settings**, then **General**, at the bottom. maxclicks asks you to confirm with **Permanently delete workspace**. Deleting a workspace cannot be undone. You and everyone on your team lose access, and all of its data goes with it: contacts, objects, events, templates, broadcasts, workflows, and history. There is no recovery and no export step built into the delete, so take anything you want to keep out first. Deletion is background work. **Deletion queued** means the request was accepted; it does not mean every record and external resource has already been removed. **Deletion is waiting** means unresolved work needs reconciliation. The workspace remains closed while this cleanup runs. Keep the reported operation details if you need support, and do not treat a delayed cleanup as permission to recreate the original work.