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"
}
}