sally docs
This page is rendered from the canonical GitHub docs. Edit on GitHub.

Sally Marketing Website Integration Kit

Sally Marketing is platform-neutral. WordPress is one possible connector, but Astro, Next.js, plain server endpoints, Webflow webhooks, and custom backends should use the same API primitives.

Integration primitives

Use restricted website tokens for website-owned forms and content gates. These tokens can only be used with embed endpoints:

POST /marketing/embed/submissions
POST /marketing/embed/content-gates/:gateKey/check

They cannot list contacts, export data, send newsletters, change campaigns, or access CRM.

Create website tokens in Sally under:

Marketing → Integrations

The Integrations page includes a snippet generator for Astro, Next.js, plain fetch handoff, and selected content gates.

Store the token as a server-side secret, for example SALLY_WEBSITE_TOKEN. For static/client-only sites, proxy through a serverless function instead of exposing a full Sally API key in browser code.

Astro example

// src/pages/api/newsletter.ts
export async function POST({ request }) {
  const form = await request.formData()

  const res = await fetch('https://sally.example.com/marketing/embed/submissions', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${import.meta.env.SALLY_WEBSITE_TOKEN}`,
      'content-type': 'application/json',
    },
    body: JSON.stringify({
      source: 'astro:newsletter-footer',
      email: form.get('email'),
      firstName: form.get('firstName'),
      fields: {
        interests: form.getAll('interests'),
        language: form.get('language'),
      },
      consent: {
        newsletter: form.get('newsletter') === 'on',
        text: 'I want to receive the newsletter',
      },
    }),
  })

  return new Response(await res.text(), {
    status: res.status,
    headers: { 'content-type': 'application/json' },
  })
}

Next.js route handler example

export async function POST(request: Request) {
  const form = await request.formData()
  const response = await fetch(`${process.env.SALLY_API_URL}/marketing/embed/submissions`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.SALLY_WEBSITE_TOKEN}`,
      'content-type': 'application/json',
    },
    body: JSON.stringify({
      source: 'nextjs:newsletter',
      email: form.get('email'),
      fields: { interests: form.getAll('interests') },
      consent: { newsletter: form.get('newsletter') === 'on', text: 'Newsletter opt-in' },
    }),
  })
  return new Response(await response.text(), { status: response.status })
}

Content gate check

const response = await fetch('https://sally.example.com/marketing/embed/content-gates/recipe-access/check', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.SALLY_WEBSITE_TOKEN}`,
    'content-type': 'application/json',
  },
  body: JSON.stringify({
    email: 'reader@example.com',
    token: accessTokenFromCookie,
  }),
})

const result = await response.json()
// { allowed: true|false, reason: '...' }

Custom field mapping

Custom fields are submitted under fields and validated against Sally field definitions:

{
  "email": "reader@example.com",
  "fields": {
    "interests": ["recipes", "events"],
    "language": "DE"
  }
}

Consent remains separate from profile data:

{
  "consent": {
    "newsletter": true,
    "text": "I want to receive the newsletter"
  }
}