# MZ3B integration toolkit

Available now, with no extra toolkit fee. Real sends still consume the account's actual message credits. This is an independent linked-device service, not Meta's official API. Delivery is not guaranteed.

## Start safely

Download these files into a private server-side directory, keeping their names:

- https://mz3b.com/examples/mz3b-node.mjs — Node.js 22+ API client.
- https://mz3b.com/examples/mz3b-tools.mjs — pure templates and outcome/readiness helpers.
- https://mz3b.com/examples/mz3b-doctor.mjs — one-shot, read-only diagnosis.

Inject `MZ3B_API_KEY` through your server secret manager. The old `MZ3B_KEY` variable remains supported as a fallback. Do not place a key in command arguments, frontend/mobile code, browser storage, repositories, chat or logs. Never print the client or request headers. Do not source an untrusted environment file.

Run on your server with the secret already injected:

```sh
node mz3b-doctor.mjs
```

This makes exactly one `GET /api/v1/account/summary` call. It never sends, debits, enrolls staff or reconnects a session. Exit 0 means fresh send readiness and all seven integration scopes; 2 means review the returned action codes; 1 means secret/network/API review is needed. None proves actual delivery. No account name, number, balance or key is printed. Importing any of these files sends nothing.

Action codes: `refresh_status` (observation older than 45 seconds, missing, unknown or invalid), `review_connection`, `review_protection` (not proof of a ban), `top_up`, `review_key_scopes`, `review_service`. For OTP-only use, call `diagnoseSummary(summary, ['account:read','verifications:send','verifications:read','verifications:check'])` instead of requiring alert scopes.

## Server integration example

The methods below make LIVE requests when called. This module does not run them automatically. Your own routes must enforce authentication, CSRF protection where applicable, per-user/device rate limits, and authorization. Bind each OTP attempt to the initiating login session AND intended phone number on your server; never let a browser choose a different attempt to authenticate with. Application database persistence is required, not supplied by an in-memory example.

```js
import { createMZ3B } from './mz3b-node.mjs';
import { orderTemplate, diagnoseSummary, explainOutcome } from './mz3b-tools.mjs';
const client = createMZ3B();

// Read-only. Return a filtered summary through your authenticated backend route.
export async function serviceCard() {
  const s = await client.summary();
  return {
    connection: s.whatsapp.connection,
    observedAt: s.whatsapp.observedAt,
    ready: diagnoseSummary(s).ready,
    paidCredits: s.billing.paidCredits,
    trial: s.billing.trial,
    billingExempt: s.billing.unlimited,
    sendCost: s.billing.sendCost,
    manage: s.links.manage,
    topUp: s.links.topUp,
  };
}

// Call only after a real user explicitly requests a WhatsApp code.
// consent contains the ACTUAL click timestamp, opaque evidence reference and login session.
export async function prepareLogin({ to, consent }) {
  return client.newVerification({
    to, locale: 'en', consentReference: consent.reference,
    consentedAt: consent.occurredAt, userSessionId: consent.userSessionId,
  });
}

// Persist the operation with a unique logical login request ID BEFORE this call.
export async function sendPersistedLogin(operation) {
  return client.sendVerification(operation); // real send; retain result.id
}

// Load the attempt from the authenticated login session, not a submitted arbitrary ID.
export async function checkBoundLogin(boundAttemptId, userEnteredCode) {
  const result = await client.checkVerification(boundAttemptId, userEnteredCode);
  return { verified: result.status === 'approved', next: explainOutcome('verification', result) };
}

// Consent must come from the authorized employee, never be generated by an AI assistant.
export async function enrollConsentingEmployee({ to, occurredAt, reference }) {
  return client.enrollStaff({ to, consentedAt: occurredAt, consentReference: reference });
}
export async function revokeEmployee(savedRecipientId) {
  return client.revokeStaff(savedRecipientId); // blocks new sends, not already in-flight ones
}

// Templates do not register consent or send anything. Restrict URLs to YOUR app's allowed origins.
export function prepareOrderAlert({ to, orderId, orderUrl, revision = '1' }) {
  const body = orderTemplate({ template: 'new_order', order: orderId, locale: 'en', revision });
  return client.newOrderAlert({ to, ...body, url: orderUrl });
}
// Persist with UNIQUE(project, employee, event-reference), not a new row on every retry.
export async function sendPersistedAlert(operation) {
  return client.sendOrderAlert(operation); // real send; retain result.id
}
export async function readAlert(savedMessageId) {
  return client.message(savedMessageId); // read/reconcile only; never resends
}
```

The API has seven scopes: `account:read`, `verifications:send`, `verifications:check`, `verifications:read`, `messages:send`, `messages:read`, `recipients:manage`. Provision only the ones needed. Invalid/revoked keys return 401 and missing scopes 403; do not rotate an existing key automatically.

## Persistence, retries and uncertainty

Keep the logical event ID, exact request JSON, idempotency key and returned attempt/message ID in an access-controlled durable outbox. Never log OTPs, full numbers or secrets. Persist before network I/O. The helper creates an operation; it does NOT persist it. Reusing the helper on every retry creates new keys and is unsafe.

For an original operation whose ID is known, read its status. If the first response is lost, preserve and retry the EXACT original request/key only according to the API contract; do not silently create a new operation, new consent timestamp or different sender. The client has no automatic retries. Retain the original reservation for `uncertain` or a network timeout. `accepted`/`pending` is not delivery; `approved` alone verifies an OTP. A 200 response is not enough.

Use the same event reference for the same employee/event. Increment a template's `revision` only for a new business event, not an HTTP retry. Changing a template or language after persistence changes the body and may cause 409. A new OTP needs fresh consent after expiration; requesting another code does not reset guessing limits. Keep uncertain attempts for reconciliation rather than speculating whether the message was sent.

Respect `Retry-After` on 429 and inspect 409 conflicts. Readiness is a snapshot, not a lock. Handle 402/503 from the actual send even if a prior summary was ready. Don't refund or retry on your own merely because a receipt is absent.

## Three different tests

1. https://mz3b.com/en/docs#toolkit: OFFLINE illustrative scenarios in the browser. No key, real phone, valid code, network send, debit or delivery proof. Not an API emulator.
2. `client.validateVerification(persistedOperation)` or `client.validateOrderAlert(persistedOperation)`: authenticated API **input validation only**. Expected `willSend:false`, `creditsCharged:0`. OTP still requires genuine fresh consent. Message validation neither enrolls nor checks employee consent, balance or live delivery. Rate limits apply.
3. `sendVerification`/`sendOrderAlert`: LIVE sending with credit consumption. Only test with an explicitly specified consenting recipient; a key prefixed `mz_test_` is still live-capable.

## Links and the account card

Order links use HTTPS with a public hostname, no embedded credentials, IP literal, localhost or nonstandard port. MZ3B does not fetch previews. Your app must authorize the employee for the requested order even after login. Keep the return destination as a same-origin relative path checked server-side; reject arbitrary external redirects and never place a secret or bearer token in the URL.

The account card should show observed connection, paid credit, eligible trial and expiry, actual send cost/exemption, and management links — not the full MZ3B dashboard or API key. Paid/trial balances are shared across the account's projects. An exempt account is represented by `billing.unlimited` and actual zero send costs, not a fake balance. A trial is usable only before its expiry and when `eligibleForSender` is true.

Use a private server cache per project/key, never a shared cross-customer cache. Poll at most every 30 seconds while the card is visible; keep one request in flight, respect the account-wide 12/minute limit, and show unknown after 45 seconds without a fresh observation. Do not persist a green badge indefinitely. This client does not schedule polling or cache credentials.

## Available versus planned

Available: existing OTP/staff-message/summary/consent APIs, server Node client, three bilingual staff templates, safe pure result helpers, one-shot server diagnosis, and eight offline learning scenarios. The templates generate text; they are NOT scheduled automation. No new billing policy or unlimited sending is introduced.

Not included: a maintained Laravel package, automatic AI replies, shared team inbox, scheduled workflows, automatic email/WhatsApp health alerts, or customer delivery webhooks. Those need their own implementation and verification. Do not advertise them as active.

Contract: https://mz3b.com/openapi.json
Complete curl examples and limits: https://mz3b.com/integration.en.md
