Docs
Campaigns

Create a draft campaign

POSThttps://api.norreach.app/v1/campaigns

Saves a new campaign as a draft — nothing is sent until it is launched (POST /campaigns/{id}/launch). The sequence may start empty; a broadcast is exactly one step and never LinkedIn. Every AI caller and brand named must be this workspace's.

campaigns:writeRole must allow: campaigns.editOperation id: create_campaign
POST/campaigns
curl -X POST "https://api.norreach.app/v1/campaigns" \
  -H "Authorization: Bearer $NORREACH_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Summer nurses — Stockholm",
  "kind": "sequence",
  "steps": [
    {
      "channel": "email",
      "wait_hours": 0,
      "template": "Subject: Nurses for the summer\n\nHi {{first_name}}, …"
    },
    {
      "channel": "sms",
      "wait_hours": 48,
      "template": "Hi {{first_name}}, did you see my email?"
    }
  ],
  "rules": [
    {
      "name": "Stop on reply",
      "when": {
        "event": "replied"
      },
      "then": {
        "action": "pause"
      }
    }
  ]
}'
Create a draft campaign
{
  "id": "3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f",
  "object": "campaign",
  "name": "Summer nurses — Stockholm",
  "status": "draft",
  "kind": "sequence",
  "channels": [
    "email",
    "sms"
  ],
  "step_count": 2,
  "brand_id": null,
  "schedule_at": null,
  "created_at": "2026-11-04T09:12:33.120Z",
  "steps": [
    {
      "channel": "email",
      "wait_hours": 0,
      "template": "Subject: Nurses for the summer\n\nHi {{first_name}}, …"
    },
    {
      "channel": "sms",
      "wait_hours": 48,
      "template": "Hi {{first_name}}, did you see my email?"
    }
  ],
  "rules": [
    {
      "name": "Stop on reply",
      "when": {
        "event": "replied"
      },
      "then": {
        "action": "pause"
      }
    }
  ],
  "settings": {
    "approve_before_send": "manual"
  },
  "stats": {
    "enrolled": 120,
    "in_progress": 80,
    "paused": 4,
    "completed": 20,
    "replied": 14,
    "meetings_booked": 2,
    "sent": 210,
    "delivered": 204,
    "opened": 96,
    "clicked": 12
  }
}

Authorizations

Authorizationstringheaderrequired

Bearer authentication: send Authorization: Bearer <key>, with an API key or an OAuth access token. The key needs the scope campaigns:write. API keys and scopes

Headers

Idempotency-Keystringheader

Any unique string (≤ 255). The same key with the same request returns the first answer for 24 hours.

Body

application/json

namestringrequired
kindenum
One of:broadcastsequencemultichannel
stepsobject[]
Show child attributes
steps[].channelenumrequired
One of:emailsmswhatsappcalllinkedin
steps[].wait_hoursnumberrequired

Hours to wait before this step (after the previous one).

steps[].templatestring

The words. Email: "Subject: <line>\n\n<body>". WhatsApp: the approved template's name. Merge tags like {{first_name}} are filled per contact.

steps[].li_kindenum

LinkedIn steps: what the step does.

One of:connectmessagelike_post
steps[].agent_idstring (uuid)

AI-call steps: which of the workspace's AI callers places the call.

steps[].retry_if_no_answerobject

AI-call steps: retry an unanswered call.

Show child attributes
steps[].retry_if_no_answer.enabledbooleanrequired

Dial again when nobody answered or the line was busy.

steps[].retry_if_no_answer.attemptsintegerrequired
steps[].retry_if_no_answer.gap_minutesintegerrequired
steps[].retry_if_no_answer.on_voicemailboolean

Also dial again when a voicemail answered.

steps[].ai_draftedboolean
steps[].coachingstring

Notes for Nora about this step.

steps[].wa_languagestring

WhatsApp steps: the approved template's language code (e.g. en_US).

steps[].wa_paramsstring[]

WhatsApp steps: the values for {{1}}…{{n}}, in order (text or merge tags).

steps[].branchobject

Wait for a signal after this step; without it the contact jumps ahead.

Show child attributes
steps[].branch.onenum

The signal that takes the branch path.

One of:acceptedrepliedopenedclicked
steps[].branch.timeout_hoursnumberrequired
steps[].branch.timeout_tointegerrequired

The step (0-based index) a contact jumps to when the signal doesn't arrive in time.

rulesobject[]
Show child attributes
rules[].namestring
rules[].statusenum
One of:activepaused
rules[].whenobjectrequired
Show child attributes
rules[].when.eventenumrequired
One of:repliedopenedclickedconnection_acceptedmeeting_bookedbounced
rules[].when.count_gteinteger
rules[].thenobjectrequired
Show child attributes
rules[].then.actionenumrequired
One of:ai_callpausecompletebook_meeting
settingsobject

Campaign settings (a sparse override of the workspace's defaults).

Show child attributes
settings.approve_before_sendenum

manual = every send waits for a person's yes. Setting auto needs the nora:autonomy scope.

One of:automanual
settings.reply_autonomouslyboolean

Nora answers replies on her own. Setting true needs the nora:autonomy scope.

settings.unsubscribeany

Unsubscribe link settings.

settings.unsubscribe_modeany

How opting out works.

settings.unsubscribe_phraseany

The opt-out wording.

settings.reengageany

Re-engagement settings.

settings.follow_up_threadany

Send follow-ups in the same email thread.

settings.prioritizeany

new, followups or both.

settings.ooo_pauseany

Pause a contact who is out of office.

settings.sendingany

Sending window and pacing.

settings.reply_handlingany

What happens when a contact replies.

settings.leads_per_dayany

New contacts started per day.

settings.prospects_per_monthany

Prospects per month.

settings.pause_on_replyany

Stop the sequence for a contact who replies.

settings.exit_on_call_connectedany

Stop the sequence for a contact once an AI call connects.

settings.call_on_intentany

Place an AI call when a contact shows intent.

settings.deliverabilityany

Deliverability settings.

settings.exclusionsany

Who is skipped at enrollment (recency_days, per_company_cap).

settings.enforce_sending_windowany

Only send inside the sending window.

settings.messagingany

Message writing preferences.

settings.schedule_atstring (date-time)

A scheduled start (launching then waits until this time).

brand_idstring (uuid)

An id (UUID).

schedule_atstring (date-time)

A scheduled start (must be in the future).

Response

201application/json— Create a draft campaign

idstring (uuid)required

An id (UUID).

object"campaign"required
namestringrequired
statusstringrequired

draft, active, paused, completed, archived or needs_review.

kindenumrequired
One of:broadcastsequencemultichannel
channelsstring[]required

The channel mix of the sequence, in order.

step_countintegerrequired
brand_idstring (uuid) | nullrequired
schedule_atstring | nullrequired
created_atstringrequired

RFC 3339 UTC timestamp with milliseconds.

stepsobject[]required

The sequence (the step shape above).

rulesobject[]required
settingsobjectrequired

The campaign's own settings override.

statsobjectrequired

The campaign's numbers right now.

Show child attributes
stats.enrolledintegerrequired
stats.in_progressintegerrequired
stats.pausedintegerrequired
stats.completedintegerrequired
stats.repliedintegerrequired
stats.meetings_bookedintegerrequired
stats.sentintegerrequired
stats.deliveredintegerrequired
stats.openedintegerrequired
stats.clickedintegerrequired

Errors

Errors are application/problem+json with a stable code.

StatusCodeMeaning
400bad_requestThe request couldn't be read.
400unknown_fieldThe request has a field this operation doesn't know.
401unauthorizedA valid API key is required.
403insufficient_scopeThis API key doesn't have the scope this operation needs.
403feature_not_enabledThe API isn't switched on for this workspace.
403role_forbiddenThe key's creator's role can't do this.
409idempotency_in_progressA request with this Idempotency-Key is still running.
413too_largeThe request body is too large.
422validation_failedThe request has invalid fields.
422idempotency_mismatchThis Idempotency-Key was used with a different request.
429rate_limitedToo many requests. Slow down and try again.
500server_errorSomething went wrong on our side.
503unavailableThe API isn't available right now. Try again shortly.