Developers
What NGAIGE is

A shop owner says what they want.
A compliant campaign goes out.

NGAIGE is mobile engagement for small businesses — consent, campaigns, delivery and the receipts that prove it. This is the door your application knocks on.

Eleven routes carry a business from “here we are” to “the campaign went out, here’s what it cost”. There is nothing else to learn.

POST /api/v1/campaigns/drafts authorization: Bearer nk_… idempotency-key: a1f2… { "name": "Saturday sourdough", "schedule": "2026-02-07T09:00:00Z" } 201 Created { "id": "cmp_8f2a…", "state": "DRAFT", "approved_by": null } Nothing sends until a person approves it.

What the platform actually does

Consent is the product

People opt in, and the proof travels with them. Every import carries exactly one carrier of consent — an attestation or a saved evidence record, never both, never neither. STOP is permanent and beats everything, everywhere.

A human always says go

A campaign moves draft → plan → submit → approve → send, and the approval names a person. That name is written into the record. Automation can draft and dispatch; it can never approve. The gate is the point.

Every number is evidence

Delivery, spend and stop counts all fold from the same events, hash-chained and verifiable. What your dashboard shows is what an auditor would find.

The whole lifecycle, and where each route sits

Eleven routes. Five of them move a campaign along the state machine; the rest register, prove consent, report and converse. The amber step is the one no key can perform.

DRAFT POST /campaigns/drafts the wording PLANNED POST /{id}/plan reach, segments, cost SUBMITTED POST /{id}/submit up for a decision APPROVED POST /{id}/approve names a human · own scope SENT POST /dispatch-requests consent + STOP screened SCOPE campaigns:approve separate on purpose

Before any of this: POST /clients registers the business and POST /imports carries the consent. After it: GET /{id}/report and GET /suppression tell you what happened, masked.

Your first campaign, in five minutes

You need one thing: an API key from your NGAIGE operator. It looks like nk_<id>_<secret> and it decides which business you are — there is no tenant to pass anywhere.

  1. Register the business. A name, and the sender ID that shows on the handset.
  2. Bring your consented list. A CSV of msisdn,consent,consented_at, plus one carrier of proof.
  3. Draft the message, then ask for a plan — who it reaches and what it should cost.
  4. Submit, then have a person approve it by name.
  5. Dispatch. Stopped numbers are skipped and named back to you.

Copy-paste versions in all five SDKs below — all five are exercised end to end against the live service, not written from the spec.

The eleven routes

Bearer auth on every call. Idempotency-Key on every mutating POST, so a retry can never send twice. Refusals arrive as application/problem+json with a machine-readable type — branch on that, never on the English.

loading…

The assistant, over the API

This is the part the product is judged on. A shop owner types a sentence; the assistant works out the audience, drafts the copy, prices it, and stops at the approval gate. One route carries the whole conversation.

One turn, one call

POST /api/v1/conversations/{conversation_id}/messages
authorization: Bearer nk_<id>_<secret>
idempotency-key: 7c1e…

{ "text": "It's dead in here on Tuesdays.
           Can we do something for the regulars?" }

The conversation id is yours to choose — one per business, or one per chat thread in your app. State lives on our side, so you hold nothing.

A turn you can render, not parse

{
  "parts":   [ { "type": "text", "text": "412 customers…" } ],
  "actions": [ { "type": "approve", "campaign_id": "cmp_8f2a…" },
               { "type": "edit",    "campaign_id": "cmp_8f2a…" } ],
  "pending": { "campaign_id": "cmp_8f2a…", "state": "SUBMITTED" },
  "state":   "awaiting_approval"
}

Build buttons from actions and a status line from state. Never scrape the English — it is written for a human and it changes.

Approval is not a tool call

The assistant can draft, plan, submit and dispatch. It cannot approve. Approval is its own route with its own scope (campaigns:approve), so no prompt, no tool loop and no leaked key can push a campaign through the gate. When the turn offers an approve action, your UI must take a real click from a real person, then call POST /campaigns/{id}/approve with their name.

What it is not

Not a chatbot bolted to a send button. The same engine drives our own apps, an operator's self-care embed and a field-sales tablet — none of them special-cased. v1 is text; media in and out is refused explicitly rather than silently dropped, so you always know what happened.

Five SDKs

Go, TypeScript, Python, Rust and Java. Every one of these examples was run against staging today — business, consent, draft, plan, submit, human approval, dispatch, report — and two of them were fixed because of what the run found.

go get github.com/con5ult/ngaige-go

c := ngaige.New(baseURL, key)
biz, _ := c.CreateBusiness(ctx, "Ta Karm Bakery", "TaKarm", "")
c.ImportConsent(ctx, biz.ID, csv, true, "", "", "")
camp, _ := c.CreateDraft(ctx, "saturday-sourdough", goal, audience,
    "Fresh sourdough from 7am Saturday. Reply STOP to opt out.",
    time.Now().Add(48*time.Hour), "")
c.GeneratePlan(ctx, camp.ID, "")
c.Submit(ctx, camp.ID, "")
c.Approve(ctx, camp.ID, "Glen Warren", "")   // a person, by name
c.Dispatch(ctx, camp.ID, targets, "")
npm i @con5ult/ngaige

const ng = new Ngaige(process.env.NGAIGE_KEY!);
const biz = await ng.createBusiness("Ta Karm Bakery", "TaKarm");
await ng.importConsent(biz.id, csv, { affirm: true });
const camp = await ng.createDraft({
  name: "saturday-sourdough", goal, audience,
  message: "Fresh sourdough from 7am Saturday. Reply STOP to opt out.",
  schedule: new Date(Date.now() + 48 * 3600_000),
});
await ng.generatePlan(camp.id);
await ng.submit(camp.id);
await ng.approve(camp.id, "Glen Warren");    // a person, by name
await ng.dispatch(camp.id, targets);
pip install ngaige

ng   = Ngaige(os.environ["NGAIGE_KEY"])
biz  = ng.create_business("Ta Karm Bakery", "TaKarm")
ng.import_consent(biz["id"], csv, affirm=True)
camp = ng.create_draft(
    name="saturday-sourdough", goal=goal, audience=audience,
    message="Fresh sourdough from 7am Saturday. Reply STOP to opt out.",
    schedule=datetime.now(timezone.utc) + timedelta(days=2))
ng.generate_plan(camp["id"])
ng.submit(camp["id"])
ng.approve(camp["id"], actor="Glen Warren")   # a person, by name
ng.dispatch(camp["id"], targets)
ngaige = "0.1"   # Cargo.toml

let ng   = Ngaige::new(&std::env::var("NGAIGE_KEY")?);
let biz  = ng.create_business("Ta Karm Bakery", "TaKarm", None)?;
ng.import_consent(&biz.id, &csv, Consent::Affirm, None)?;
let camp = ng.create_draft(
    "saturday-sourdough", &goal, &audience,
    "Fresh sourdough from 7am Saturday. Reply STOP to opt out.",
    SystemTime::now() + Duration::from_secs(48 * 3600), None)?;
ng.generate_plan(&camp.id, None)?;
ng.submit(&camp.id, None)?;
ng.approve(&camp.id, "Glen Warren", None)?;   // a person, by name
ng.dispatch(&camp.id, &targets, None)?;
// com.con5ult:ngaige:0.1.0  — zero dependencies, java.net.http only

Ngaige ng   = new Ngaige(System.getenv("NGAIGE_KEY"));
Map<String,Object> biz = ng.createBusiness("Ta Karm Bakery", "TaKarm", null);
ng.importConsent((String) biz.get("id"), csv, true, null, null, null);
Map<String,Object> camp = ng.createDraft(
    "saturday-sourdough", goal, audience,
    "Fresh sourdough from 7am Saturday. Reply STOP to opt out.",
    Instant.now().plus(2, ChronoUnit.DAYS), null);
ng.generatePlan((String) camp.get("id"), null);
ng.submit((String) camp.get("id"), null);
ng.approve((String) camp.get("id"), "Glen Warren", null);  // a person, by name
ng.dispatch((String) camp.get("id"), targets, null);

Whole seconds

A schedule carrying fractional seconds is accepted as a draft and then refused at plan forever — a campaign that can never send. Every SDK truncates for you.

Idempotency, done right

Keys are generated per call and reused on retry, which is what makes a retry safe. A dropped connection can never send a campaign twice.

Typed refusals

Problems arrive as values you switch on — consent-required, scope-required, not-approved — never English you have to match.

Try it, right here

Paste a key and make a real call against the live service. Your key stays in this browser tab — it is never stored, logged or sent anywhere but the API.

Reads are safe to poke at. POST calls do real things — they create real records against whatever tenant your key belongs to.

Two applications, built on exactly these routes

TownCrier

An independent third-party app: a shop owner pastes a key and runs campaigns from their phone. Live segment and cost meter, the consent rules surfaced kindly, and the whole approval pipeline as a visible stepper.

towncrier.staging.con5ult.com

Luzzu Mobile

An operator embedding NGAIGE inside its own subscriber app — including a staff console that mints subscriber keys live. The operator keeps its own world; the key is the only bridge.

luzzu.staging.con5ult.com

The long-form guides