Newsletter API
The Newsletter API manages the same contacts, consent, campaigns and delivery
queues as the Grigora dashboard. Base URL:
https://api.grigora.co/general/api/v1/newsletter.
Management operations use POST with JSON, following the existing Grigora v1
API convention. The machine-readable OpenAPI 3.1 contract is available at
GET /general/api/v1/newsletter/openapi.json. It is also included in the public
Swagger documentation at /api-docs/public.
What existed before this API
/project/newsletter/*: authenticated dashboard operations using the logged-in user's session and project role. These are not API-key endpoints./general/newsletter/*: public signup, confirmation, tracking, unsubscribe, preference-center and provider webhook endpoints. Tokens protect recipient actions; provider-specific verification protects inbound delivery events./general/project/form/entry: website form submissions and newsletter opt-in.
Use the new API v1 namespace for server-side integrations. Never put project API keys, OAuth tokens or SMTP credentials in a website's browser code.
Authentication and permissions
For a project API key, send Authorization: YOUR_PROJECT_API_KEY. The key's
project is authoritative. The key creator must still be a member, and their
current role determines allowed operations. Legacy keys use the platform's
existing owner-resolution behavior. A conflicting project_id is rejected.
For OAuth, send Authorization: Bearer YOUR_ACCESS_TOKEN and include
project_id in the JSON body. The project must be part of the grant. The
original authorizing user's current role is checked, even if another platform
endpoint internally resolves an effective owner account.
| OAuth scope | Capabilities | Minimum project role |
|---|---|---|
newsletter:read | Read contacts, audiences, campaigns and reports | Member |
newsletter:write | Manage contacts, audiences, drafts, templates and imports | Editor |
newsletter:send | Send, schedule, resume, test and retry delivery | Editor |
newsletter:admin | Sender settings, connections and deleting lists | Administrator |
Scopes are independent: newsletter:send does not imply newsletter:read.
Exports require an editor. Provider webhook URL discovery requires an
administrator and newsletter:admin. Scope grants never override project roles.
Existing OAuth integrations must add the required newsletter scopes to their app
registration and obtain a fresh user authorization; a refresh token does not
expand the original grant.
Responses, limits and retries
Successful JSON responses have data and request_id. Errors have
error.code, error.message, error.retryable, optional error.details, and
request_id. Authentication failures can use the existing platform auth error
format. X-Request-ID identifies requests that reach the newsletter handler.
- Reads: 600 requests/minute/project.
- Mutations: 120 requests/minute/project.
- All API keys and OAuth callers share the project's corresponding bucket.
- Responses include
RateLimit-Limit,RateLimit-Remaining, andRateLimit-Reset(seconds until reset). - A
429includesRetry-Afterin seconds. Use exponential backoff with jitter. - Database rate-limit failures fail closed with
503. - Page sizes default to 50 and cannot exceed 200. Unknown request properties and
out-of-range limits return
400; they are not silently ignored.
Every mutation requires Idempotency-Key: YOUR_STABLE_KEY, or the equivalent
idempotency_key JSON field. Use a different key for each intended action, and
reuse the original key and body after a network timeout or retryable error.
Receipts are persisted in PostgreSQL and scoped to project, actor and operation.
A duplicate completed request returns the stored status and response with
Idempotent-Replayed: true, without running it again. Reusing a key with a
different body returns 409 IDEMPOTENCY_CONFLICT. A concurrent request returns
409 REQUEST_IN_PROGRESS.
Completed responses are retained for at least 24 hours, then expire during hourly worker maintenance. After expiry, the same key can execute again. An interrupted request may have taken effect before its receipt was completed; its pending receipt deliberately does not expire or automatically execute again. Inspect the campaign/contact/import/job before reconciling that operation. This is duplicate-execution protection, not a claim of an atomic transaction across every business operation and the receipt store.
202 means an operation was accepted or queued, not that emails were delivered.
Poll jobs/get, imports/get, or the campaign status and report as appropriate.
Subscribe a contact
curl https://api.grigora.co/general/api/v1/newsletter/contacts/upsert \
-H 'Authorization: YOUR_PROJECT_API_KEY' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: signup-EXTERNAL_EVENT_ID' \
--data '{
"email": "ada@example.com",
"first_name": "Ada",
"last_name": "Lovelace",
"list_ids": ["YOUR_LIST_ID"],
"custom_fields": {"city": "London"},
"consent_text": "Signed up through the newsletter form"
}'
Create the city field using custom-fields/create before collecting it.
Custom values use the project's definitions and types. contacts/update changes
profile data without subscribing anyone. contacts/upsert requires consent
text, respects suppression and list double opt-in, queues confirmation messages
when needed, and does not resubscribe an opt-out unless explicitly requested.
API list-membership subscriptions and API bulk list additions follow the same
confirmation rules. Imports retain their existing explicit consent/confirmation
options; review those options during dry-run.
For an email-only upsert with no list IDs, the contact can exist without list
membership. Pass the intended list IDs to receive those list's campaigns.
contacts/delete remains available; there is no anonymize/erase operation.
Pagination and filtering
Pass limit and the prior response's data.next_cursor. Stop when it is null.
Keep filters unchanged while paging. Configuration resources, campaigns,
templates, imports and jobs use immutable creation time plus ID as the cursor
order. Email documents and private import storage keys are omitted from list
responses; fetch individual resources when needed. templates/list also returns
a bounded system gallery of built-in template metadata; pass its IDs to
campaigns/create to seed a design.
Contacts support search, status, list_id, list_status, tag_id and
filter.rules. Suppressions support search and reason. Campaigns support
search and status. Jobs support kinds and statuses arrays. Lists omit
archived entries unless include_archived is true.
Campaign events and recipients retain their existing after_id pagination:
pass the last item's ID, with the same filters, until the next page is empty.
contacts/export streams text/csv with backpressure; it does not return a JSON
envelope and is deliberately not an MCP tool.
A segment rule group looks like:
{
"op": "and",
"rules": [
{"field": "global_status", "operator": "eq", "value": "subscribed"},
{"field": "custom.city", "operator": "eq", "value": "London"}
]
}
Supported fields include standard contact fields, custom.FIELD_KEY, list,
tag, segment and engagement conditions. Groups can nest up to four levels with
at most 60 rules. Use segments/preview to validate and count an audience before
saving. For contacts/bulk, provide either explicit contact_ids (up to 1000)
or a nonempty filter, and any action-specific list/tag IDs. Adding contacts to
a list also requires consent_text. A rule group with an empty rules array
explicitly matches all contacts; use that deliberately, especially for deletion.
The worker processes API bulk actions asynchronously in bounded pages without
loading the entire audience into memory. Filters are evaluated as each page is
read, rather than representing a frozen snapshot. For filtered jobs, progress
reports a processed count with no total until completion. This is not a
general-purpose workflow engine.
Create, preview and deliver a campaign
- Read
settings/get,connections/listand the intended audience. - Create a draft with
campaigns/create, supplyingsubject,editor_jsonandaudience_spec.template_idcan seed a saved or built-in template. - Call
renderfor HTML/text preview andcampaigns/readinessfor send checks. - Call
campaigns/test-sendwith up to five test addresses when authorized. - Call
campaigns/send, orcampaigns/schedulewith an ISO timestamp including an offset (for example2027-02-18T10:30:00+05:30) and timezone. - Poll
campaigns/get,campaigns/stats,campaigns/eventsandcampaigns/recipients. Pause, resume or cancel through their named actions.
An audience specification uses includes and exclusions:
{
"include": [{"type": "list", "id": "YOUR_LIST_ID"}],
"exclude": [{"type": "tag", "id": "YOUR_EXCLUDED_TAG_ID"}]
}
schema/get documents the email block format and available merge tags. Example:
{
"version": 1,
"settings": {},
"blocks": [
{"id": "greeting", "type": "text", "props": {
"html": "<p>Hello {{ contact.full_name | default: \"friend\" }}!</p>"
}}
]
}
To import existing email markup, use an html block with props.html; the
newsletter sanitizer still applies. track_opens and track_clicks can be set
on a campaign to control tracking.
The same renderer, footer checks, consent rules, suppression checks, plan limits, provider limits and leased delivery outbox are used by the dashboard and API. API access does not bypass sending readiness or provider quotas.
Bulk imports
imports/create accepts a csv string up to 5 MiB and an optional filename.
It stages the existing import pipeline. imports/dry-run accepts import_id,
a column-index mapping such as {"0":"email","1":"first_name"}, and import
options. Inspect the validation result before imports/commit, which queues
processing. Poll imports/get for counters and row errors. Split larger datasets
into separate imports; do not submit them as thousands of serial upserts.
MCP actions
The 78 newsletter MCP wrappers are prepared for future use, but their imports, registration, and builder policy entries are commented out. No newsletter actions are discoverable or callable through MCP, including website-builder sessions. The HTTP API is available independently. The operation table lists the deferred MCP names for reference only.
Operation reference
The OpenAPI contract is authoritative for parameters and validation. All paths
below are relative to /general/api/v1/newsletter and use POST.
| API path | OAuth scope suffix | MCP action (deferred) |
|---|---|---|
/schema/get | read | get_newsletter_schema |
/overview | read | get_newsletter_overview |
/contacts/list | read | list_newsletter_contacts |
/contacts/stats | read | get_newsletter_contact_stats |
/contacts/get | read | get_newsletter_contact |
/contacts/upsert | write | upsert_newsletter_contact |
/contacts/update | write | update_newsletter_contact |
/contacts/delete | write | delete_newsletter_contacts |
/contacts/unsubscribe | write | unsubscribe_newsletter_contact |
/contacts/reset-status | write | reset_newsletter_contact_status |
/contacts/membership | write | set_newsletter_membership |
/contacts/tags | write | tag_newsletter_contacts |
/contacts/bulk | write | bulk_manage_newsletter_contacts |
/contacts/export | read | CSV stream only |
/custom-fields/list | read | list_newsletter_custom_fields |
/custom-fields/create | write | create_newsletter_custom_field |
/custom-fields/update | write | update_newsletter_custom_field |
/custom-fields/delete | write | delete_newsletter_custom_field |
/lists/list | read | list_newsletter_lists |
/lists/create | write | create_newsletter_list |
/lists/update | write | update_newsletter_list |
/lists/delete | admin | delete_newsletter_list |
/tags/list | read | list_newsletter_tags |
/tags/create | write | create_newsletter_tag |
/tags/update | write | update_newsletter_tag |
/tags/delete | write | delete_newsletter_tag |
/segments/list | read | list_newsletter_segments |
/segments/get | read | get_newsletter_segment |
/segments/preview | read | preview_newsletter_segment |
/segments/create | write | create_newsletter_segment |
/segments/update | write | update_newsletter_segment |
/segments/delete | write | delete_newsletter_segment |
/segments/materialize | write | materialize_newsletter_segment |
/suppressions/list | read | list_newsletter_suppressions |
/suppressions/add | write | suppress_newsletter_addresses |
/suppressions/remove | write | remove_newsletter_suppression |
/settings/get | read | get_newsletter_settings |
/settings/update | admin | update_newsletter_settings |
/connections/list | read | list_newsletter_connections |
/connections/create | admin | create_newsletter_connection |
/connections/update | admin | update_newsletter_connection |
/connections/delete | admin | delete_newsletter_connection |
/connections/test | admin | test_newsletter_connection |
/connections/verify-dns | admin | verify_dns_newsletter_connection |
/connections/webhook-urls | admin | get_newsletter_provider_webhook_urls |
/templates/list | read | list_newsletter_templates |
/templates/get | read | get_newsletter_template |
/templates/create | write | create_newsletter_template |
/templates/update | write | update_newsletter_template |
/templates/delete | write | delete_newsletter_template |
/render | read | render_newsletter_email |
/campaigns/list | read | list_newsletter_campaigns |
/campaigns/get | read | get_newsletter_campaign |
/campaigns/create | write | create_newsletter_campaign |
/campaigns/update | write | update_newsletter_campaign |
/campaigns/audience-count | read | count_newsletter_audience |
/campaigns/readiness | read | check_newsletter_campaign |
/campaigns/duplicate | write | duplicate_newsletter_campaign |
/campaigns/delete | write | delete_newsletter_campaign |
/campaigns/unschedule | write | unschedule_newsletter_campaign |
/campaigns/pause | write | pause_newsletter_campaign |
/campaigns/cancel | write | cancel_newsletter_campaign |
/campaigns/send | send | send_newsletter_campaign |
/campaigns/resume | send | resume_newsletter_campaign |
/campaigns/requeue-failed | send | requeue_failed_newsletter_campaign |
/campaigns/schedule | send | schedule_newsletter_campaign |
/campaigns/test-send | send | send_newsletter_test |
/campaigns/stats | read | get_newsletter_campaign_stats |
/campaigns/events | read | list_newsletter_campaign_events |
/campaigns/recipients | read | list_newsletter_campaign_recipients |
/imports/create | write | create_newsletter_import |
/imports/list | read | list_newsletter_imports |
/imports/get | read | get_newsletter_import |
/imports/dry-run | write | validate_newsletter_import |
/imports/commit | write | commit_newsletter_import |
/imports/cancel | write | cancel_newsletter_import |
/jobs/list | read | list_newsletter_jobs |
/jobs/get | read | get_newsletter_job |
/jobs/requeue | send | requeue_newsletter_job |
Performance and compatibility
This API adopts established integration patterns: bounded cursor pages, queued
bulk processing, project-wide throttling, persistent retry receipts, versioned
schemas and shared API/MCP contracts. Mailchimp recommends pagination and batches
for large workloads (fundamentals);
Klaviyo documents account-level throttles and 429 handling
(rate limits).
Those are design references, not benchmark results.
These endpoints cover Grigora's newsletter management capabilities. Production throughput, latency and deliverability parity with Mailchimp or Klaviyo have not been established. The API does not add their complete automation/flows, SMS, ecommerce event-ingestion or outbound integration-webhook platforms. Provider webhooks here are inbound bounce/complaint events; integrations can poll campaign events for delivery changes.