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.
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.
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.
- Register the business. A name, and the sender ID that shows on the handset.
- Bring your consented list. A CSV of
msisdn,consent,consented_at, plus one carrier of proof. - Draft the message, then ask for a plan — who it reaches and what it should cost.
- Submit, then have a person approve it by name.
- 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.
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.
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.
The long-form guides
- Channel API — the full route reference, every example executed against a live boot
- Reference client guide — the conventions any language must implement
- Executive overview — the one-pager for decision makers
- openapi.json — machine-readable, generate a client in any language