# MZ3B one-key integration — version 1.3

English UI and guide: https://mz3b.com/en/docs
Arabic guide: https://mz3b.com/integration.md
Machine-readable contract: https://mz3b.com/openapi.json
Server-only reference client: https://mz3b.com/examples/mz3b-node.mjs

## Scope and safety

After project setup and a confirmed WhatsApp link, your application needs only this documentation URL and a project key. The key selects the project and sender automatically. OTP and staff alerts use the same selection policy; an explicitly enabled pool may choose different senders for distinct operations, never for retries within one operation. Dashboard login is needed only for owner actions such as relinking or top-up.

Inject MZ3B_KEY from your server secret manager. Never put it in browser/mobile code, browser storage, chat, repositories or logs. Do not use verbose curl/header tracing or log OTP bodies. Existing mz_test_ keys can send real messages; the prefix is NOT a simulation mode. Do not rotate keys or unlink numbers to perform an integration.

| Endpoint capability | Scope |
| --- | --- |
| Send / validate OTP | verifications:send |
| Check OTP | verifications:check |
| Read OTP attempt | verifications:read |
| Send / validate alert | messages:send |
| Read alert | messages:read |
| Enroll, list, revoke staff | recipients:manage |
| Read summary | account:read |

Existing unrestricted keys remain compatible. Scoped and revoked keys cannot bypass their restrictions. The summary returns effective scopes. POST /api/keys is an owner browser-session endpoint, not a way for project keys to create more keys; it accepts optional scopes with browser mutation protection.

## Readiness and billing — no send or debit

```bash
curl --silent --show-error --max-time 20 \
  'https://mz3b.com/api/v1/account/summary' \
  -H "Authorization: Bearer $MZ3B_KEY"
```

Read project.name, ready, reasons, whatsapp.connection and whatsapp.observedAt. The sender phone is masked. Actual account-shared paid credit is billing.paidCredits; trial remaining, expiry and sender eligibility are in billing.trial. Read billing.sendCost rather than hardcoding a price or administrative exemption. Follow links.manage and links.topUp.

A normal OTP or alert reserves one message. Eligible active trial credit is used first, then paid credit. The trial is up to 20 messages once over 7 days from the first successful phone link; it does not renew on number/project deletion. Purchased credits do not expire monthly. Reads, verification checks and validation cost no message credits. Authorized owner exemptions are explicit, not fabricated balances.

The summary records key usage and rate limiting but does not send, reserve credit, charge, or restart WhatsApp. Connected does not necessarily mean ready: balance or protection may block sending. On unknown observation or a network failure, display “Unable to confirm connection,” not an old green badge or an assumed ban. Poll every 30 seconds while visible, invalidate readiness after 45 seconds, avoid overlaps and respect Retry-After. Limit: 12 reads/account/minute.

## Verification codes

Obtain actual consent and an E.164 recipient. Create an operation UUID and durably store it with the exact request body BEFORE sending. Placeholder values below are not real recipients or proof of consent.

Private otp-request.json:
```json
{
  "to": "USER_APPROVED_E164",
  "locale": "en",
  "purpose": "authentication",
  "consent": {
    "granted": true,
    "occurredAt": "ACTUAL_CONSENT_ISO_8601_TIME",
    "reference": "login:unique-consent-reference"
  },
  "context": { "userSessionId": "opaque-end-user-session-id" }
}
```

Consent must be within 10 minutes when creating a new protected operation. Use an opaque login-session reference, not an authentication cookie. Older applications retain their to/locale contract; new protected applications require consent and context.userSessionId.

```bash
# Input validation only: NO send, reservation or debit.
curl --silent --show-error --max-time 20 \
  'https://mz3b.com/api/v1/verifications/validate' \
  -H "Authorization: Bearer $MZ3B_KEY" \
  -H "Idempotency-Key: $OTP_OPERATION_ID" \
  -H 'Content-Type: application/json' --data-binary @otp-request.json

# REAL SEND: only with specific recipient consent. Reserves one message.
curl --silent --show-error --max-time 20 \
  'https://mz3b.com/api/v1/verifications' \
  -H "Authorization: Bearer $MZ3B_KEY" \
  -H "Idempotency-Key: $OTP_OPERATION_ID" \
  -H 'Content-Type: application/json' --data-binary @otp-request.json

# VERIFICATION_ID is the attempt id returned by send, not a project/session id.
curl --silent --show-error --max-time 20 \
  "https://mz3b.com/api/v1/verifications/$VERIFICATION_ID" \
  -H "Authorization: Bearer $MZ3B_KEY"

# Private received-code.json: {"code":"CODE_ENTERED_BY_RECIPIENT"}
curl --silent --show-error --max-time 20 \
  "https://mz3b.com/api/v1/verifications/$VERIFICATION_ID/check" \
  -H "Authorization: Bearer $MZ3B_KEY" \
  -H 'Content-Type: application/json' --data-binary @received-code.json
```

Save the returned id. pending means accepted, not delivered. WhatsApp codes have 6 digits and expire after 5 minutes. ONLY status=approved means successful verification; HTTP 200 alone is insufficient. pending and failed are not success. 410 OTP_EXPIRED means expired. Protected projects share 5 checks per recipient over 10 minutes, not 5 for every new code. Never guess a code or automatically repeat a check whose lost response may already have consumed an attempt.

## Staff consent and order alerts

The application must verify the recipient is an authorized employee and actually consented. API assertions do not independently establish consent. Keep evidence in your application and send an opaque reference. Enroll once after genuine recent opt-in.

Private staff-consent.json:
```json
{
  "to": "STAFF_APPROVED_E164",
  "role": "staff",
  "consent": {
    "granted": true,
    "occurredAt": "ACTUAL_CONSENT_ISO_8601_TIME",
    "reference": "staff:consent:unique-reference"
  }
}
```

```bash
# Enrollment sends nothing and charges nothing.
curl --silent --show-error --max-time 20 \
  'https://mz3b.com/api/v1/message-recipients' \
  -H "Authorization: Bearer $MZ3B_KEY" \
  -H 'Content-Type: application/json' --data-binary @staff-consent.json

# List masked entries; save each employee's id for revocation.
curl --silent --show-error --max-time 20 \
  'https://mz3b.com/api/v1/message-recipients' \
  -H "Authorization: Bearer $MZ3B_KEY"

# Revoke future consent. This cannot cancel a send that has already started.
curl --silent --show-error --max-time 20 -X DELETE \
  "https://mz3b.com/api/v1/message-recipients/$STAFF_RECIPIENT_ID" \
  -H "Authorization: Bearer $MZ3B_KEY"
```

Persist order-alert.json and ORDER_OPERATION_ID for the logical event:
```json
{
  "to": "STAFF_APPROVED_E164",
  "text": "Order 42 is ready for review.",
  "reference": "order:42:ready:v1",
  "url": "https://shop.example.com/orders/42"
}
```

The link must be public HTTPS without credentials, IP literals, private/local hosts or a custom port. Maximum text length: 1500 characters. URL: 1000 characters. Reference: up to 100 opaque characters from letters, digits and ._:-. Do not include secrets or customer details in references. Put the link in url, not additional links in text.

MZ3B does not open the URL or generate a preview. Your application must enforce staff login and order permissions, then restore the intended order through a validated same-site return path. Do not include API keys or long-lived login tokens in links.

```bash
# Syntax-only validation: does NOT establish staff consent or readiness.
curl --silent --show-error --max-time 20 \
  'https://mz3b.com/api/v1/messages/validate' \
  -H "Authorization: Bearer $MZ3B_KEY" \
  -H "Idempotency-Key: $ORDER_OPERATION_ID" \
  -H 'Content-Type: application/json' --data-binary @order-alert.json

# REAL SEND to an authorized, consenting employee.
curl --silent --show-error --max-time 20 \
  'https://mz3b.com/api/v1/messages' \
  -H "Authorization: Bearer $MZ3B_KEY" \
  -H "Idempotency-Key: $ORDER_OPERATION_ID" \
  -H 'Content-Type: application/json' --data-binary @order-alert.json

# Read/reconcile the original result; no resend or second debit.
curl --silent --show-error --max-time 20 \
  "https://mz3b.com/api/v1/messages/$MESSAGE_ID" \
  -H "Authorization: Bearer $MZ3B_KEY"
```

The response includes id, status, billing, delivery and a status URL. 201 accepted is provider acceptance, not receipt. 202 reserved/uncertain is pending or unknown and does not authorize another operation. failed with delivery.status=not_sent means definitive non-sending and one refund. A read may reconcile the provider's earlier result but never sends. Guaranteed alert delivery receipts are not currently available; delivery.confirmed remains false.

Project-scoped idempotency also checks recipient plus event reference. Changing only the idempotency key cannot duplicate that event. A genuine later update needs a new event reference, such as order:42:shipped:v1. A changed body with a used operation key or event reference conflicts. Retrying a failed operation returns its failure, without another send. Durable integration/gateway records retain references, hashes, masked numbers and results, not the raw recipient, message text or link; the delivery provider necessarily processes message content.

Limits: 25 active employees and 100 total recipient records/project, 10 enrollments or reactivations/account/day. Sender rate: 20/minute shared with OTP; account minute limits are also shared. Staff alert limits: 10/minute and 200/day per recipient across the service, 1000/day/account. These are safety ceilings, not bulk-marketing allowances.

## Node.js integration example

Use Node.js 22+ and review the reference client. Importing it makes no request. Its default request timeout is 20 seconds, redirects are rejected, and secrets/bodies are not logged. You must provide durable application storage; the client is not a durable queue.

```javascript
import { createMZ3B, MZ3BError } from './mz3b-node.mjs';
const mz3b = createMZ3B(); // MZ3B_KEY injected by a server secret manager

export async function inspectReadiness() {
  return mz3b.summary(); // No send / no debit
}

export async function deliverPersistedOtp(operation) {
  // Create with newVerification ONLY after actual opt-in, then persist in your DB.
  await mz3b.validateVerification(operation); // Input validation, not delivery proof
  const state = await mz3b.summary();
  if (!state.ready) return { ready: false, reasons: state.reasons };
  try {
    const result = await mz3b.sendVerification(operation);
    // Persist result.id against the SAME operation.
    return result;
  } catch (error) {
    if (!(error instanceof MZ3BError)) throw error;
    // Preserve operation.idempotencyKey and operation.body unchanged.
    // If error.id exists, read mz3b.verification(error.id).
    // Never create a new operation automatically after uncertainty.
    return { error: error.message, id: error.id, retryAfter: error.retryAfter };
  }
}

export async function verifyReceivedCode(id, receivedCode) {
  const result = await mz3b.checkVerification(id, receivedCode);
  return result.status === 'approved';
}

export async function deliverPersistedStaffAlert(operation) {
  // Staff was enrolled after genuine opt-in; operation was durably stored first.
  await mz3b.validateOrderAlert(operation);
  const result = await mz3b.sendOrderAlert(operation);
  // Save result.id. For reserved/uncertain, use mz3b.message(result.id).
  return result; // accepted is NOT confirmed delivery
}
```

Create an OTP operation with newVerification({to,locale:'en',consentedAt,consentReference,userSessionId}). Enroll an employee with enrollStaff({to,consentedAt,consentReference}); revoke with revokeStaff(id). Create an alert with newOrderAlert({to,text,reference,url}). Times and consent evidence must be actual, not invented to pass validation. Persist the generated UUID and body before invoking send. Do not regenerate either on retry.

## Errors, timeouts and duplicate protection

| Result | Required handling |
| --- | --- |
| 400 / 413 / 415 | Correct the request format, size or content type. |
| 401 | Key invalid, revoked or project inactive; contact owner without disclosing the key. |
| 402 | Insufficient credit. Read summary and top-up link. |
| 403 | Missing scope, unauthorized recipient or opt-out. Do not bypass it. |
| 409 IDEMPOTENCY_KEY_REUSED | Restore the original body instead of changing the key. |
| 409 REQUEST_IN_PROGRESS | Honor Retry-After and retain the original OTP operation. |
| 410 OTP_EXPIRED | Reject the code; a new send needs a new user action and consent. |
| 429 | Wait for Retry-After. Do not rotate accounts or senders to evade it. |
| 202 uncertain / 502 DELIVERY_STATE_UNAVAILABLE / network timeout | Message may have been sent. Read its status or repeat the exact original operation, never a new one. |
| 502 with failed/not_sent | Definitive non-sending, credit refunded once. Repeating the operation returns that failure. |
| 503 | Service, session or reservation unavailable; inspect readiness and preserve the operation. |

If the send response is lost before the ID arrives, repeat the exact original key/body to retrieve it. Provider failures after OTP reservation retain the original attempt ID. Never fabricate a newer consent timestamp for an old operation: a saved-operation retry uses the original reservation time and sender, even after the consent freshness window. New operations and syntax validation still require actual fresh consent.

A pending or uncertain attempt remains tied to its original sender even if assignments or pool order change. Gateway reconciliation history lasts 15 minutes; after history loss or expiry the outcome may require review. Missing history is not proof of failure or a refund entitlement. Database idempotency remains while the operation record exists; this version has no automatic purge cycle for these records.

## Sender pools

Owners may explicitly enable a pool in the project dashboard. Existing projects retain their current selection until the owner saves a pool. No new key is required.

- priority: ready primary, then a backup BEFORE first sending, for a confirmed technical outage or owner-set daily limit.
- balanced: choose the lowest proportional use among ready senders. Atomic reservations prevent overshooting the cap under concurrency.
- Daily cap: integer 1–10000, UI default 30, counted per physical sender across all projects and OTP/staff alerts. Resets at 00:00 UTC. Reserved and uncertain operations retain their slot. Only confirmed non-sending releases it.
- Never switch sender after attempting an operation, even if the outcome is failed or uncertain. Reuse its original key and body.
- Protection, warnings, restrictions or an undiagnosed FAILED state stop automatic selection for review. Unavailable member observations make readiness uncertain. A cap of 30 does not guarantee avoidance of platform restrictions.
- All account, recipient and rate limits remain in force. Additional numbers do not create additional free trials.

The summary optionally adds routing: mode, ready, reasons, dayTimezone=UTC, resetsAt and senders[]. Each sender includes its name, masked number, observed status/time, priority, dailyLimit, usedToday, available and reason, not internal session identifiers. whatsapp summarizes the current candidate; reading this does not reserve a sender.

429 SENDER_DAILY_LIMIT_REACHED: wait for routing.resetsAt. 503 SENDER_POOL_PROTECTED / SENDER_POOL_REVIEW_REQUIRED: owner review, no automatic rotation. SENDER_POOL_STATUS_UNAVAILABLE: observation failed. SENDER_POOL_LOCAL_STATE_PENDING: local synchronization incomplete. SENDER_POOL_NO_CONNECTED_NUMBER: no available connected sender. RESERVATION_UNAVAILABLE: retain the operation; do not assume reservation did not happen.

Pool configuration endpoints are owner browser-session APIs, NOT project-key APIs: GET/PUT /api/applications/{id}/sender-pool. PUT accepts {"mode":"balanced","senders":[{"id":"OWNED_NUMBER_UUID","dailyLimit":30}]}, 1–8 owned members within account limits, protected by browser origin checks and x-mz3b-client: dashboard. Saving changes the ordered selection and shared cap, not number/session ownership or linkage.

## Available versus not available

Available: project-scoped keys and permissions; OTP send/check/read; live summary with actual billing; consenting staff alerts; recipient revocation; durable idempotency and atomic debit; input-only validation; optional sender pools. Existing OTP contracts and keys remain compatible.

Not available: guaranteed delivery receipts or customer-facing delivery webhooks; unsolicited bulk/marketing sends; automatic WhatsApp AI replies; email OTP. Gemini BYOK storage and isolated provider tests do not activate WhatsApp auto-replies. Local mocked tests do not prove receipt or completion of the customer's application integration. Perform a separately approved real end-to-end test before claiming it works for that application.
