2 api
vmanne-poc edited this page 2026-10-26 10:39:35 -04:00

Using the API

Postak has a REST API, so your own scripts and systems can work with a project without anyone signing in: add subscribers from your website or CRM, keep groups in step with another system, create and send campaigns, and read reports.

Scripts sign in with a project API key. A key works for one project only, and only on the routes listed below.

Get a key

A project owner creates the key in Settings → API keys inside the project. The key is shown once, when it's created, so copy it straight into the place your script reads it from. See API Keys for creating, listing, and revoking keys.

A key looks like postak_ab12cd34_ followed by 43 more letters, digits, - and _. The first part (postak_ab12cd34) is the key's prefix: Postak shows it in the key list so you can tell keys apart. It isn't secret on its own.

Make a request

  • Base URL: your Postak address followed by /api, for example https://postak.example.org/api (the same address you open in the browser, through nginx). On a local install it's http://localhost:3000/api.
  • Key: send it in the Authorization header as a Bearer token: Authorization: Bearer postak_…. Keys in the query string aren't accepted.
  • Body: JSON, with Content-Type: application/json.
  • Project ID: every route starts with /projects/<project id>/. The ID is in the browser's address bar when you open the project: …/projects/<project id>.

The examples below use these shell variables:

export POSTAK_API=https://postak.example.org/api
export POSTAK_KEY=postak_ab12cd34_...        # your key; keep it out of scripts you share
export PROJECT=7f9c0b6e-1111-2222-3333-444455556666

Add a contact

curl -s -X POST "$POSTAK_API/projects/$PROJECT/contacts" \
  -H "Authorization: Bearer $POSTAK_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email": "sam@example.org", "firstName": "Sam", "data": {"plan": "pro"}}'

The answer is the new contact, with its id. Contacts added this way are Active. An address that's already in the project gets 409. To add or update many people at once, use POST /projects/<id>/contacts/import with up to 1,000 rows per request.

Add contacts to a group

# Find the group's id
curl -s "$POSTAK_API/projects/$PROJECT/groups" -H "Authorization: Bearer $POSTAK_KEY"

curl -s -X POST "$POSTAK_API/projects/$PROJECT/groups/$GROUP/members" \
  -H "Authorization: Bearer $POSTAK_KEY" \
  -H "Content-Type: application/json" \
  -d '{"contactIds": ["<contact id>", "<another contact id>"]}'

The answer counts what happened: { "matched": 2, "added": 2, "alreadyMembers": 0, "notFound": 0 }. Up to 1,000 contact IDs per request.

Create a campaign for a group

Look up the sender and template to use first (GET /projects/<id>/senders and GET /projects/<id>/templates). Then:

curl -s -X POST "$POSTAK_API/projects/$PROJECT/campaigns" \
  -H "Authorization: Bearer $POSTAK_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "subject": "June update",
    "bodyType": "markdown",
    "body": "Hi {{ contact.first_name | default: \"there\" }},\n\nHere is what changed in June.",
    "senderId": "<sender id>",
    "templateId": "<template id>",
    "groupIds": ["<group id>"]
  }'

The campaign is saved as a Draft, and the answer includes its id. The API doesn't pick the project's default template for you: leave out templateId and the email is sent without a template. You can also send to segments (segmentIds) and leave people out (excludeGroupIds, excludeSegmentIds), as in the campaign editor.

Send it

curl -s -X POST "$POSTAK_API/projects/$PROJECT/campaigns/$CAMPAIGN/send" \
  -H "Authorization: Bearer $POSTAK_KEY"

The answer is { "status": "SENDING", "recipients": 120 } (or SENT straight away when nobody matches). Delivery carries on in the background. To send later instead, use POST …/campaigns/<id>/schedule with { "scheduledAt": "2026-11-02T08:00:00Z" }.

Read the report

curl -s "$POSTAK_API/projects/$PROJECT/campaigns/$CAMPAIGN/report" \
  -H "Authorization: Bearer $POSTAK_KEY"

The report has the same numbers as the Report page: sent, opened, clicked, unsubscribed, bounced, complaints, failed, the rates, and how many emails are still waiting. …/recipients, …/links, and …/activity give the detail, and …/recipients.csv downloads the recipient list.

What a key can do

Keys work on these routes, all under /api/projects/<project id>:

Area Method and path
Contacts GET /contacts, GET /contacts/count, GET /contacts/export.csv, POST /contacts, POST /contacts/import, GET /contacts/:id, PATCH /contacts/:id, DELETE /contacts/:id, POST /contacts/:id/unsubscribe
Groups GET /groups, POST /groups, GET /groups/:id, PATCH /groups/:id, DELETE /groups/:id, GET /groups/:id/members, POST /groups/:id/members, POST /groups/:id/members/remove
Segments GET /segments, POST /segments, POST /segments/preview, GET /segments/for-contact/:contactId, GET /segments/:id, GET /segments/:id/preview, GET /segments/:id/export.csv, PATCH /segments/:id, DELETE /segments/:id
Campaigns GET /campaigns, POST /campaigns, GET /campaigns/audience, GET /campaigns/ab-test/limits, GET /campaigns/:id, PATCH /campaigns/:id, DELETE /campaigns/:id, POST /campaigns/:id/duplicate, POST /campaigns/:id/send, POST /campaigns/:id/schedule, POST /campaigns/:id/unschedule, GET /campaigns/:id/report, GET /campaigns/:id/links, GET /campaigns/:id/recipients, GET /campaigns/:id/recipients.csv, GET /campaigns/:id/activity, POST /campaigns/:id/ab-test/winner
Senders (read only) GET /senders, GET /senders/:id. Passwords and secret keys are never included
Templates (read only) GET /templates, GET /templates/:id

Everything else needs a person who is signed in:

  • Answered with 403 (the route belongs to an area above, but keys can't use it): campaign previews (POST /campaigns/preview) and test sends (POST /campaigns/:id/test); creating, changing, testing, or deleting senders; creating, previewing, changing, cloning, or deleting templates.
  • Answered with 401 (keys aren't accepted at all): projects, members, users, system info, sign-up forms, daily reports, and API keys themselves. A key can't create or revoke keys.

The request and answer of each route are the same as for the web app. A/B tests can be set up and their winner picked through the API too.

When something goes wrong

Errors come back as JSON with a statusCode and a message, for example {"statusCode": 401, "message": "Invalid API key.", "error": "Unauthorized"}.

Status Meaning
401 The key is missing, mistyped, made up, or revoked (always the message Invalid API key., whatever the route), or the route doesn't accept keys at all (see above).
403 The key is fine, but this route isn't open to keys: API keys can't be used for this endpoint.
404 The project in the path isn't the key's project (Project not found., the same answer as for a project that doesn't exist), or the contact, group, campaign, or other item doesn't exist in the project.
413 The request body is too large. Campaign and template routes accept JSON up to 4 MB, and every other route up to 100 KB. Requests without an Authorization: Bearer header get the 100 KB limit everywhere.
429 Too many requests. Wait and try again. A 429 for the key's own limit has a Retry-After header with the number of seconds to wait.
400 The request itself isn't valid (a missing field, an ID that isn't a UUID, a rule the API refuses). The message says what to fix.
409 A conflict, such as a contact that already exists or a group that a draft campaign still uses.

The order of the checks

Postak checks a request in a fixed order and answers with the first problem it finds. Before it looks at the key at all:

  • The size of the body: 413 when it's over the limit.
  • On campaign and template routes, nginx allows each address at most 5 requests per second that change something (anything but GET), with a burst of 50. More than that gets 429 before the body is read.

Then:

  1. 429 when your address has sent too many requests (any request, with any key or none).
  2. 401 when the route doesn't accept keys at all.
  3. 401 when the key isn't valid or has been revoked.
  4. 429 when the key has used up its own limit.
  5. 404 when the project in the path isn't the key's project.
  6. 403 when the route isn't open to keys.
  7. Then the route's own checks: 400, 404 for a missing item, 409, and so on.

So a revoked key always gets 401, even on a route keys could never use, and a valid key never learns anything about another project.

Rate limits

  • Per key: 300 requests a minute by default. Every request with a valid key counts, including ones that are refused with 403 or 404. Your administrator can change the limit.
  • Per address: the usual limits for everyone, whether signed in or using a key (by default 600 requests a minute per route, and 60 a minute for adding or removing group members).
  • Writes to campaigns and templates: at most 5 requests a second from one address through nginx, with a burst of 50.

If a server runs several Postak backends, each one counts on its own. Build your script to slow down and retry after a 429 rather than relying on exact numbers.

Keep keys safe

  • Use keys only on servers and in scripts you control. Never put a key in a web page, a mobile app, or anything else that runs on other people's devices: anyone who can read it can use it.
  • Create one key per integration (for example "Website sign-ups" and "CRM sync"), so you can revoke one without stopping the others.
  • Revoke keys you no longer use, and revoke a key straight away if it may have leaked. Then create a new one.
  • A key belongs to the project, not to the person who created it. It keeps working when that person leaves the project, so review the list from time to time.