Docs

HTTP API: messaging

Email and push campaigns to your app's users: templates, audiences defined by SQL, previews, sends, deliveries, usage and opt-outs.

Updated Oct 18, 2026

Send push notifications and emails to your app's users: templates, SQL audiences, sends, the delivery log and opt-outs (Messaging: push and email to your users).

Base URLhttps://<project-ref>.sovarias.in — your project URL from Connect.
KeysSend the project's key as apikey. Calls made for a signed-in user also send Authorization: Bearer <access token>.
ErrorsA status code and {"detail": …} — a sentence, or {code, message} for errors your code may branch on (HTTP API).

Templates

GET/api/v1/projects/{project_id}/messaging/templates

Message templates.

Who
A project owner or admin, or the service-role key (for example from a function).

Path parameters

project_idstringrequired
The project.

Responses

  • 200Success.
  • 401No valid session or key.
curl -s "$URL/api/v1/projects/f0c3e9a2-1b4d-4e7f-8a6c-2d9b5e3f1a07/messaging/templates" \
  -H "apikey: $ANON_KEY" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
Response
{
  "templates": [
    {
      "name": "order-shipped",
      "description": "Sent when an order ships",
      "category": "transactional",
      "push": {
        "title": "Your order shipped",
        "body": "Order #{{ order_id }} is on its way."
      },
      "email": {
        "subject": "Order #{{ order_id }} shipped",
        "html": "<p>Hi {{ name }}, your order is on its way.</p>",
        "text": "Hi {{ name }}, your order is on its way."
      },
      "channels": [
        "push",
        "email"
      ],
      "updated_by": "ana@acme.com",
      "created_at": "2026-10-08T10:00:00+00:00",
      "updated_at": "2026-10-12T09:00:00+00:00"
    }
  ]
}

GET/api/v1/projects/{project_id}/messaging/templates/{name}

One template.

Who
A project owner or admin, or the service-role key (for example from a function).

Path parameters

namestringrequired
The template.
project_idstringrequired
The project.

Responses

  • 200Success.
  • 404No such template.
  • 401No valid session or key.
curl -s "$URL/api/v1/projects/f0c3e9a2-1b4d-4e7f-8a6c-2d9b5e3f1a07/messaging/templates/order-shipped" \
  -H "apikey: $ANON_KEY" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
Response
{
  "name": "order-shipped",
  "description": "Sent when an order ships",
  "category": "transactional",
  "push": {
    "title": "Your order shipped",
    "body": "Order #{{ order_id }} is on its way."
  },
  "email": {
    "subject": "Order #{{ order_id }} shipped",
    "html": "<p>Hi {{ name }}, your order is on its way.</p>",
    "text": "Hi {{ name }}, your order is on its way."
  },
  "channels": [
    "push",
    "email"
  ],
  "updated_by": "ana@acme.com",
  "created_at": "2026-10-08T10:00:00+00:00",
  "updated_at": "2026-10-12T09:00:00+00:00"
}

PUT/api/v1/projects/{project_id}/messaging/templates/{name}

Create or replace a template with push and/or email content.

{{ variables }} are filled per user from the send's variables and the audience's extra columns. Marketing messages respect each user's opt-out and email carries a one-click unsubscribe; transactional ones (receipts, account notices) don't. Names: lowercase letters, digits, _, -. The plan limits how many (msg.templates_per_project) (Messaging: push and email to your users).

Who
A project owner or admin, or the service-role key (for example from a function).

Path parameters

namestringrequired
Template name.
project_idstringrequired
The project.

Body

descriptionstringoptional
categorystringoptionaldefault transactional
One of transactional, marketing
pushobjectoptional
{title, body, image?, data?, sound?, badge?}.
emailobjectoptional
{subject, html?, text?}.

Responses

  • 200Success.
  • 422A template needs push or email content (or a field is invalid).
  • 403PLAN_LIMIT_REACHED.
  • 401No valid session or key.
curl -s -X PUT "$URL/api/v1/projects/f0c3e9a2-1b4d-4e7f-8a6c-2d9b5e3f1a07/messaging/templates/order-shipped" \
  -H "apikey: $ANON_KEY" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"description": "Sent when an order ships", "category": "transactional", "push": {"title": "Your order shipped", "body": "Order #{{ order_id }} is on its way."}, "email": {"subject": "Order #{{ order_id }} shipped", "html": "<p>Hi {{ name }}, your order is on its way.</p>", "text": "Hi {{ name }}, your order is on its way."}}'
Response
{
  "name": "order-shipped",
  "description": "Sent when an order ships",
  "category": "transactional",
  "push": {
    "title": "Your order shipped",
    "body": "Order #{{ order_id }} is on its way."
  },
  "email": {
    "subject": "Order #{{ order_id }} shipped",
    "html": "<p>Hi {{ name }}, your order is on its way.</p>",
    "text": "Hi {{ name }}, your order is on its way."
  },
  "channels": [
    "push",
    "email"
  ],
  "updated_by": "ana@acme.com",
  "created_at": "2026-10-08T10:00:00+00:00",
  "updated_at": "2026-10-12T09:00:00+00:00"
}

DELETE/api/v1/projects/{project_id}/messaging/templates/{name}

Delete a template.

Who
A project owner or admin, or the service-role key (for example from a function).

Path parameters

namestringrequired
The template.
project_idstringrequired
The project.

Responses

  • 204Success, with no body.
  • 401No valid session or key.
curl -s -X DELETE "$URL/api/v1/projects/f0c3e9a2-1b4d-4e7f-8a6c-2d9b5e3f1a07/messaging/templates/order-shipped" \
  -H "apikey: $ANON_KEY" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Audiences

GET/api/v1/projects/{project_id}/messaging/audiences

Saved audiences (SQL that selects users).

Who
A project owner or admin, or the service-role key (for example from a function).

Path parameters

project_idstringrequired
The project.

Responses

  • 200Success.
  • 401No valid session or key.
curl -s "$URL/api/v1/projects/f0c3e9a2-1b4d-4e7f-8a6c-2d9b5e3f1a07/messaging/audiences" \
  -H "apikey: $ANON_KEY" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
Response
{
  "audiences": [
    {
      "name": "inactive-30d",
      "description": "No order in 30 days",
      "sql": "select user_id, name from customers where last_order_at < now() - interval '30 days'",
      "last_count": 412,
      "last_run_at": "2026-10-18T08:00:00+00:00",
      "updated_by": "ana@acme.com",
      "updated_at": "2026-10-15T09:00:00+00:00"
    }
  ]
}

GET/api/v1/projects/{project_id}/messaging/audiences/{name}

One audience.

Who
A project owner or admin, or the service-role key (for example from a function).

Path parameters

namestringrequired
The audience.
project_idstringrequired
The project.

Responses

  • 200Success.
  • 401No valid session or key.
curl -s "$URL/api/v1/projects/f0c3e9a2-1b4d-4e7f-8a6c-2d9b5e3f1a07/messaging/audiences/inactive-30d" \
  -H "apikey: $ANON_KEY" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
Response
{
  "name": "inactive-30d",
  "description": "No order in 30 days",
  "sql": "select user_id, name from customers where last_order_at < now() - interval '30 days'",
  "last_count": 412,
  "last_run_at": "2026-10-18T08:00:00+00:00",
  "updated_by": "ana@acme.com",
  "updated_at": "2026-10-15T09:00:00+00:00"
}

PUT/api/v1/projects/{project_id}/messaging/audiences/{name}

Save an audience: a SELECT that returns user_id (other columns become variables).

Runs read-only with a 10-second limit, up to 20,000 characters of SQL. Rows whose user_id isn't an app user of the project are skipped.

Who
A project owner or admin, or the service-role key (for example from a function).

Path parameters

namestringrequired
Audience name.
project_idstringrequired
The project.

Body

sqlstringrequired
The query.
1–20,000 characters
descriptionstring | nulloptional
A note.
0–300 characters

Responses

  • 200Success.
  • 422Not a SELECT (or WITH … SELECT), no user_id column, or the query failed.
  • 403PLAN_LIMIT_REACHED.
  • 401No valid session or key.
curl -s -X PUT "$URL/api/v1/projects/f0c3e9a2-1b4d-4e7f-8a6c-2d9b5e3f1a07/messaging/audiences/inactive-30d" \
  -H "apikey: $ANON_KEY" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"sql": "select user_id, name from customers where last_order_at < now() - interval '30 days'", "description": "No order in 30 days"}'
Response
{
  "name": "inactive-30d",
  "description": "No order in 30 days",
  "sql": "select user_id, name from customers where last_order_at < now() - interval '30 days'",
  "last_count": 412,
  "last_run_at": "2026-10-18T08:00:00+00:00",
  "updated_by": "ana@acme.com",
  "updated_at": "2026-10-15T09:00:00+00:00"
}

DELETE/api/v1/projects/{project_id}/messaging/audiences/{name}

Delete an audience.

Who
A project owner or admin, or the service-role key (for example from a function).

Path parameters

namestringrequired
The audience.
project_idstringrequired
The project.

Responses

  • 204Success, with no body.
  • 401No valid session or key.
curl -s -X DELETE "$URL/api/v1/projects/f0c3e9a2-1b4d-4e7f-8a6c-2d9b5e3f1a07/messaging/audiences/inactive-30d" \
  -H "apikey: $ANON_KEY" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

POST/api/v1/projects/{project_id}/messaging/audiences/preview

Try audience SQL: how many users it matches and a sample.

Who
A project owner or admin, or the service-role key (for example from a function).

Path parameters

project_idstringrequired
The project.

Body

sqlstringrequired
The query.
1–20,000 characters

Returns

usersinteger
Rows that are app users of the project.
over_limit, limitboolean, integer
More rows than one send may reach (msg.max_recipients_per_send).
samplearray<object>
Up to 10 matching rows.

Responses

  • 200Success.
  • 401No valid session or key.
  • 422The body is missing a required field or has a field of the wrong type.
curl -s -X POST "$URL/api/v1/projects/f0c3e9a2-1b4d-4e7f-8a6c-2d9b5e3f1a07/messaging/audiences/preview" \
  -H "apikey: $ANON_KEY" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"sql": "select user_id, name from customers where last_order_at < now() - interval '30 days'"}'
Response
{
  "columns": [
    "user_id",
    "name"
  ],
  "rows": 412,
  "users": 409,
  "over_limit": false,
  "limit": 50000,
  "not_app_users": 3,
  "sample": [
    {
      "user_id": "3c8f1e2a-9b47-4d6e-a5c1-7f0e2d4b8a93",
      "name": "Ana"
    }
  ]
}

POST/api/v1/projects/{project_id}/messaging/audiences/{name}/preview

Preview a saved audience.

Who
A project owner or admin, or the service-role key (for example from a function).

Path parameters

namestringrequired
The audience.
project_idstringrequired
The project.

Responses

  • 200Success.
  • 401No valid session or key.
curl -s -X POST "$URL/api/v1/projects/f0c3e9a2-1b4d-4e7f-8a6c-2d9b5e3f1a07/messaging/audiences/inactive-30d/preview" \
  -H "apikey: $ANON_KEY" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
Response
{
  "columns": [
    "user_id",
    "name"
  ],
  "rows": 412,
  "users": 409,
  "over_limit": false,
  "limit": 50000,
  "not_app_users": 3,
  "sample": [
    {
      "user_id": "3c8f1e2a-9b47-4d6e-a5c1-7f0e2d4b8a93",
      "name": "Ana"
    }
  ]
}

Sending and delivery

POST/api/v1/projects/{project_id}/messaging/send

Send push and/or email to app users. Answers at once (202); the send continues in the background.

Use a template, or content directly. dry_run: true counts recipients and renders a preview without sending. Recipients per send and monthly sends are limited by the plan (msg.max_recipients_per_send, push.sends_month, email.sends_month).

Who
A project owner or admin, or the service-role key (for example from a function).

Path parameters

project_idstringrequired
The project.

Body

templatestringoptional
A template name (or give content).
contentobjectoptional
{push?, email?} like a template's.
toobjectrequired
One of user_ids (array), audience (name), sql, topic (push topic) or all_users: true.
channelsarray<string>optional
push, email (default: every channel the content has).
variablesobjectoptional
Values for {{ variables }}.
categorystringoptional
One of transactional, marketing
dry_runbooleanoptional

Returns

statusstring
sending, then done or failed.
countsobject
Per channel: eligible, no_device/no_email, opted_out, then delivery results (sent, failed).

Responses

  • 202Accepted — the work continues in the background.
  • 404No such template or audience.
  • 409PUSH_NOT_CONFIGURED — no FCM/APNs credentials.
  • 403MESSAGING_DISABLED/PUSH_DISABLED by support, or a plan limit.
  • 422No content, no to, more than 10,000 user_ids, bad variables (at most 50; text, numbers or booleans), or channels the content doesn't have.
  • 401No valid session or key.
curl -s -X POST "$URL/api/v1/projects/f0c3e9a2-1b4d-4e7f-8a6c-2d9b5e3f1a07/messaging/send" \
  -H "apikey: $ANON_KEY" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"template": "order-shipped", "to": {"user_ids": ["3c8f1e2a-9b47-4d6e-a5c1-7f0e2d4b8a93"]}, "variables": {"order_id": "1042", "name": "Ana"}}'
Response
{
  "id": "5d2c…",
  "status": "sending",
  "template": "order-shipped",
  "audience": null,
  "target": "users",
  "channels": [
    "push",
    "email"
  ],
  "category": "transactional",
  "recipients": 1,
  "counts": {
    "push": {
      "eligible": 1,
      "no_device": 0,
      "opted_out": 0
    },
    "email": {
      "eligible": 1,
      "no_email": 0,
      "opted_out": 0
    }
  },
  "dry_run": false,
  "error": null,
  "actor": "ana@acme.com",
  "created_at": "2026-10-18T10:00:00+00:00",
  "finished_at": null,
  "not_app_users": 0
}

GET/api/v1/projects/{project_id}/messaging/sends

Recent sends.

Who
A project owner or admin, or the service-role key (for example from a function).

Path parameters

project_idstringrequired
The project.

Query parameters

limitintegeroptionaldefault 50
How many (1–200).
1–200

Responses

  • 200Success.
  • 401No valid session or key.
curl -s "$URL/api/v1/projects/f0c3e9a2-1b4d-4e7f-8a6c-2d9b5e3f1a07/messaging/sends" \
  -H "apikey: $ANON_KEY" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
Response
{
  "sends": [
    {
      "id": "5d2c…",
      "status": "done",
      "template": "order-shipped",
      "audience": null,
      "target": "users",
      "channels": [
        "push",
        "email"
      ],
      "category": "transactional",
      "recipients": 1,
      "counts": {
        "push": {
          "eligible": 1,
          "no_device": 0,
          "opted_out": 0,
          "sent": 1
        },
        "email": {
          "eligible": 1,
          "no_email": 0,
          "opted_out": 0,
          "sent": 1
        }
      },
      "dry_run": false,
      "error": null,
      "actor": "ana@acme.com",
      "created_at": "2026-10-18T10:00:00+00:00",
      "finished_at": "2026-10-18T10:00:04+00:00"
    }
  ]
}

GET/api/v1/projects/{project_id}/messaging/sends/{send_id}

One send and its counts.

Who
A project owner or admin, or the service-role key (for example from a function).

Path parameters

send_idstringrequired
The send.
project_idstringrequired
The project.

Responses

  • 200Success.
  • 404No such send.
  • 401No valid session or key.
curl -s "$URL/api/v1/projects/f0c3e9a2-1b4d-4e7f-8a6c-2d9b5e3f1a07/messaging/sends/5d2c…" \
  -H "apikey: $ANON_KEY" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
Response
{
  "id": "5d2c…",
  "status": "done",
  "template": "order-shipped",
  "audience": null,
  "target": "users",
  "channels": [
    "push",
    "email"
  ],
  "category": "transactional",
  "recipients": 1,
  "counts": {
    "push": {
      "eligible": 1,
      "no_device": 0,
      "opted_out": 0
    },
    "email": {
      "eligible": 1,
      "no_email": 0,
      "opted_out": 0
    }
  },
  "dry_run": false,
  "error": null,
  "actor": "ana@acme.com",
  "created_at": "2026-10-18T10:00:00+00:00",
  "finished_at": "2026-10-18T10:00:04+00:00"
}

GET/api/v1/projects/{project_id}/messaging/deliveries

Per-user delivery log.

Kept for the plan's msg.delivery_log_days.

Who
A project owner or admin, or the service-role key (for example from a function).

Path parameters

project_idstringrequired
The project.

Query parameters

send_idstring | nulloptional
One send.
user_idstring | nulloptional
One user.
statusstring | nulloptional
sent, failed, opted_out, no_device, no_email.
0–16 characters
limitintegeroptionaldefault 100
How many (1–500).
1–500
offsetintegeroptionaldefault 0
How many to skip.
0–1000000

Responses

  • 200Success.
  • 401No valid session or key.
curl -s "$URL/api/v1/projects/f0c3e9a2-1b4d-4e7f-8a6c-2d9b5e3f1a07/messaging/deliveries?send_id=5d2c…" \
  -H "apikey: $ANON_KEY" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
Response
{
  "deliveries": [
    {
      "send_id": "5d2c…",
      "user_id": "3c8f1e2a-9b47-4d6e-a5c1-7f0e2d4b8a93",
      "channel": "push",
      "status": "sent",
      "error": null,
      "at": "2026-10-18T10:00:02+00:00"
    },
    {
      "send_id": "5d2c…",
      "user_id": "3c8f1e2a-9b47-4d6e-a5c1-7f0e2d4b8a93",
      "channel": "email",
      "status": "sent",
      "error": null,
      "at": "2026-10-18T10:00:03+00:00"
    }
  ]
}

GET/api/v1/projects/{project_id}/messaging/usage

This month's sends and the plan's messaging limits.

Who
A project owner or admin, or the service-role key (for example from a function).

Path parameters

project_idstringrequired
The project.

Responses

  • 200Success.
  • 401No valid session or key.
curl -s "$URL/api/v1/projects/f0c3e9a2-1b4d-4e7f-8a6c-2d9b5e3f1a07/messaging/usage" \
  -H "apikey: $ANON_KEY" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
Response
{
  "this_month": {
    "sends": 214,
    "push_sent": 18420,
    "email_sent": 3902
  },
  "limits": {
    "msg.templates_per_project": 100,
    "msg.audiences_per_project": 50,
    "msg.max_recipients_per_send": 50000,
    "msg.delivery_log_days": 30,
    "push.sends_month": 1000000,
    "email.sends_month": 2000
  }
}

Opt-outs

PUT/api/v1/projects/{project_id}/messaging/users/{user_id}/optouts

Opt a user in or out of marketing messages (for example from your own settings screen or support).

Who
A project owner or admin, or the service-role key (for example from a function).

Path parameters

user_idstringrequired
The user.
project_idstringrequired
The project.

Body

channelstringrequired
One of push, email, all
opted_outbooleanrequired

Returns

marketingobject
Channel → receives marketing messages.

Responses

  • 200Success.
  • 404Not an app user of the project.
  • 401No valid session or key.
curl -s -X PUT "$URL/api/v1/projects/f0c3e9a2-1b4d-4e7f-8a6c-2d9b5e3f1a07/messaging/users/3c8f1e2a-9b47-4d6e-a5c1-7f0e2d4b8a93/optouts" \
  -H "apikey: $ANON_KEY" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"channel": "email", "opted_out": true}'
Response
{
  "marketing": {
    "push": true,
    "email": false
  }
}

GET/api/v1/messaging/preferences

The signed-in user's marketing preferences.

Who
A signed-in app user: Authorization: Bearer <access token>, plus the project's apikey.

Responses

  • 200Success.
  • 401No valid session or key.
curl -s "$URL/api/v1/messaging/preferences" \
  -H "apikey: $ANON_KEY" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
Response
{
  "marketing": {
    "push": true,
    "email": false
  }
}

PUT/api/v1/messaging/preferences

The signed-in user turns marketing push or email on or off.

Who
A signed-in app user: Authorization: Bearer <access token>, plus the project's apikey.

Body

marketingobject<string, boolean>required
{push?, email?} → true/false.

Responses

  • 200Success.
  • 401No valid session or key.
  • 422The body is missing a required field or has a field of the wrong type.
curl -s -X PUT "$URL/api/v1/messaging/preferences" \
  -H "apikey: $ANON_KEY" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"marketing": {"email": false}}'
Response
{
  "marketing": {
    "push": true,
    "email": false
  }
}

GET/api/v1/messaging/unsubscribe

The unsubscribe page linked from marketing emails (shows a button; mail scanners don't unsubscribe anyone).

Who
PUBLIC (a signed link).

Query parameters

pstringrequired
Project (from the link).
ustringrequired
User (from the link).
cstringoptionaldefault email
Channel (default email).
tstringrequired
Signature.

Responses

  • 200An HTML page with an Unsubscribe button.
  • 400The link is invalid.
curl -s "$URL/api/v1/messaging/unsubscribe" \
  -H "apikey: $ANON_KEY"

POST/api/v1/messaging/unsubscribe

One-click unsubscribe (List-Unsubscribe-Post, RFC 8058) and the page's button.

Who
PUBLIC (a signed link).

Query parameters

pstringrequired
Project.
ustringrequired
User.
cstringoptionaldefault email
Channel.
tstringrequired
Signature.

Responses

  • 200An HTML confirmation page.
  • 400The link is invalid.
curl -s -X POST "$URL/api/v1/messaging/unsubscribe" \
  -H "apikey: $ANON_KEY"
Something unclear or missing on this page?Tell us