Documentation

Contacts

Storing people in AppEngine — public forms, CRUD, properties, tags, segments.

A contact is the master identity record in CRM — email, phone, address, plus arbitrary properties and tags. Every other CRM entity (leads, opportunities, conversations, tickets) eventually references a contact. Contacts can be created publicly (from a form), via import, by sign-up, or by direct CRUD against the data API.

Public form submission

The CRM controller exposes two @PublicRoute() endpoints designed for unauthenticated marketing pages.

Map a form to a record

POST/crm/contact-form/post/:app/:nameNo auth

:app is the app namespace (often the org's site name); :name is the form name registered in the admin. The body uses URL-encoded form fields. AppEngine maps the fields by name onto a contact record using the form's configured field mapping.

<form action="https://appengine.appmint.io/crm/contact-form/post/marketing/contact-us"
      method="POST" enctype="application/x-www-form-urlencoded">
  <input type="hidden" name="orgid" value="my-org" />
  <input name="email" type="email" required />
  <input name="name" />
  <textarea name="message"></textarea>
  <button type="submit">Send</button>
</form>

The response is a redirect (302) to the form's configured successUrl, with the new contact's SK appended as ?ref=<sk>.

JSON variant

POST/crm/contact-form/json/:app/:nameNo auth

Same idea, but accepts and returns JSON — what you want from a Next.js form handler:

await fetch('/api/crm/contact-form/json/marketing/contact-us', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', orgid: ORG_ID },
  body: JSON.stringify({
    email: '[email protected]',
    name: 'Alice',
    message: 'Interested in pricing',
    source: 'pricing-page',
  }),
});

Get the form definition

GET/crm/form/:nameNo auth
POST/crm/form/submit/:name/:email?No auth

GET /crm/form/:name returns the form's field definitions — useful when you want to render a form dynamically from the CMS rather than hard-coding the HTML. POST /crm/form/submit/:name validates and submits.

Anti-spam

Public form endpoints are rate-limited per IP and per email. Add a hCaptcha or Turnstile token via the admin form config — AppEngine validates it server-side before creating the record.

Contact CRUD

The contact collection is the master "contact" datatype. Use the generic data endpoints:

POST/repository/create/contactJWT
GET/repository/get/contact/:idJWT
PUT/repository/get/contact/:idJWT
DELETE/repository/get/contact/:idJWT
POST/repository/find/contactJWT
// Create
const created = await fetch('/api/data/contact', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', orgid: ORG_ID, Authorization: `Bearer ${jwt}` },
  body: JSON.stringify({
    data: {
      email: '[email protected]',
      name: 'Alice Doe',
      phone: '+15551234567',
      tags: ['newsletter', 'pricing-page'],
      properties: { plan_interest: 'pro', company: 'Acme' },
    },
  }),
}).then(r => r.json());

// Find by attribute
const found = await fetch('/api/repository/find-by-attribute/contact/email/[email protected]', {
  headers: { orgid: ORG_ID, Authorization: `Bearer ${jwt}` },
}).then(r => r.json());

The contact data shape:

{
  email: string;            // unique within org
  name?: string;
  firstName?: string;
  lastName?: string;
  phone?: string;
  addresses?: Address[];
  tags?: string[];
  properties?: Record<string, any>;  // free-form custom fields
  source?: string;          // where they came from (form name, ad campaign, etc.)
  consent?: { marketing: boolean; sms: boolean; updatedAt: string };
  customerGroups?: string[];
  ownerId?: string;         // assigned user SK
}

Tags

Tags are simple string arrays on the contact. Add or remove via the data API or via the dedicated tag endpoints (also dispatch automation events tag.added / tag.removed):

// Add a tag — fires `contact.tagged`
await fetch(`/api/data/contact/${contactSk}/tag/add`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', orgid: ORG_ID, Authorization: `Bearer ${jwt}` },
  body: JSON.stringify({ tags: ['high-value'] }),
});

Tag-driven flows (e.g. "send onboarding email when tag signed-up is added") live entirely in the Automation module — no extra wiring on the contact side.

Properties

Properties are the custom-field bucket. Define them once in the admin (with type, label, validation) and AppEngine renders them in form builders and segment editors. The contact stores them as plain data.properties.<key>.

// Update a single property
await fetch(`/api/data/contact/${contactSk}`, {
  method: 'PATCH',
  headers: { 'Content-Type': 'application/json', orgid: ORG_ID, Authorization: `Bearer ${jwt}` },
  body: JSON.stringify({ data: { properties: { plan_interest: 'enterprise' } } }),
});

PATCH does a deep-merge on data — only the keys you send change.

Segments

A segment is a saved query. Segments are stored under the audience controller:

POST/crm/marketing/audiences/segmentsJWT
GET/crm/marketing/audiences/segmentsJWT
GET/crm/marketing/audiences/segments/:idJWT
PUT/crm/marketing/audiences/segments/:idJWT
DELETE/crm/marketing/audiences/segments/:idJWT

The segment body is a Dynamic Query — the same query structure used everywhere in AppEngine:

{
  "name": "High-value newsletter subscribers",
  "criteria": {
    "and": [
      { "field": "tags", "op": "contains", "value": "newsletter" },
      { "field": "properties.lifetime_value", "op": ">=", "value": 1000 }
    ]
  }
}

Segments are evaluated lazily — the membership query runs whenever a campaign sends, when an audience preview is requested, or when an automation flow asks "is this contact in segment X". They're not materialized lists; the count moves as your contact data moves.

Estimate reach

POST/crm/marketing/audiences/estimate-reachJWT
GET/crm/marketing/audiences/segments/:id/insightsJWT
GET/crm/marketing/audiences/segments/:id1/overlap/:id2JWT

estimate-reach accepts a draft segment criteria and returns the count + breakdown by source / channel. insights returns the same for an existing segment plus engagement metrics. overlap shows how many contacts are in both segments.

What writes fire

Every contact write fires events into the automation bus:

  • contact.created — new record
  • contact.updated — any field change
  • contact.tagged / contact.untagged — with tag payload
  • contact.consent_changed — when data.consent flips

Use these as triggers in Automation flows; nothing else is required to wire them up.