openapi: 3.1.0
info:
  title: Emaratel Transactional Email API
  version: 0.2.0
  description: Multi-tenant REST control plane for Emaratel TE.
servers:
  - url: https://api.te.emaratel.com
security: [{bearerAuth: []}]
paths:
  /v1/auth/login:
    post: {security: [], summary: Create a customer or administrator session, responses: {'200': {description: Authenticated}, '401': {description: Invalid credentials}}}
  /v1/auth/magic/request:
    post: {security: [], summary: Email a single-use passwordless sign-in link (signup and login), responses: {'200': {description: Sent (always, no account enumeration)}, '429': {description: Too many recent requests}}}
  /v1/auth/magic/consume:
    post: {security: [], summary: Exchange a magic-link token for a session, creating tenant and owner on first use, responses: {'200': {description: Authenticated}, '401': {description: Link invalid or expired}}}
  /v1/auth/methods:
    get: {security: [], summary: Which sign-in doors are available (magic link, google), responses: {'200': {description: Methods}}}
  /v1/auth/google/start:
    get: {security: [], summary: Redirect to Google's consent screen, responses: {'302': {description: Redirect to Google}, '404': {description: Google sign-in not configured}}}
  /v1/auth/google/callback:
    get: {security: [], summary: Complete Google sign-in and create a session, creating tenant and owner on first use, responses: {'302': {description: Redirect to the console}}}
  /v1/auth/logout:
    post: {summary: End the current session, responses: {'204': {description: Signed out}}}
  /v1/me:
    get: {summary: Current identity, tenant and profile completeness, responses: {'200': {description: Identity}}}
  /v1/me/profile:
    post: {summary: Complete the company profile wizard, responses: {'200': {description: Saved}, '422': {description: Missing required fields or terms not accepted}}}
  /v1/email/send:
    post:
      summary: Queue an email
      parameters:
        - {in: header, name: Idempotency-Key, schema: {type: string}}
      requestBody:
        required: true
        content: {application/json: {schema: {$ref: '#/components/schemas/SendEmail'}}}
      responses: {'202': {description: Queued}, '401': {description: Unauthorized}, '422': {description: Validation or policy rejection}, '429': {description: Quota or rate limit}}
  /v1/email:
    get:
      summary: Search tenant messages
      parameters:
        - {in: query, name: q, schema: {type: string}, description: Matches sender, subject or any recipient}
        - {in: query, name: status, schema: {type: string}}
        - {in: query, name: from, schema: {type: string, format: date}}
        - {in: query, name: to, schema: {type: string, format: date}}
        - {in: query, name: limit, schema: {type: integer, maximum: 200}}
      responses: {'200': {description: Messages}}
  /v1/metrics/breakdown:
    get:
      summary: Per-sender sending breakdown with totals (Reports Summary)
      parameters:
        - {in: query, name: from, schema: {type: string, format: date}}
        - {in: query, name: to, schema: {type: string, format: date}}
      responses: {'200': {description: Breakdown}}
  /v1/metrics/summary:
    get:
      summary: Daily sending metrics, totals and bounce/spam rates with thresholds
      parameters: [{in: query, name: days, schema: {type: integer, minimum: 7, maximum: 90, default: 30}}]
      responses: {'200': {description: Metrics}}
  /v1/email/{id}:
    get:
      summary: Get a message and its event timeline
      parameters: [{in: path, name: id, required: true, schema: {type: string}}]
      responses: {'200': {description: Message}, '404': {description: Not found}}
  /v1/usage:
    get: {summary: Current quota and rate limit, responses: {'200': {description: Usage}}}
  /v1/exports:
    get: {summary: List this tenant's data exports, responses: {'200': {description: Exports}}}
    post: {summary: Queue an async CSV export (activity or suppressions), responses: {'202': {description: Queued}}}
  /v1/exports/{id}/download:
    get:
      summary: Download a finished export as CSV
      parameters: [{in: path, name: id, required: true, schema: {type: string}}]
      responses: {'200': {description: CSV file}, '409': {description: Not ready}}
  /v1/plans:
    get: {security: [], summary: 'Active plans with AED/USD/INR prices (public, feeds the pricing page)', responses: {'200': {description: Plans}}}
  /v1/billing:
    get: {summary: Tenant billing status + latest subscription, responses: {'200': {description: Billing}}}
  /v1/billing/portal:
    post: {summary: 'Open the hosted billing portal (invoices, card management, cancel) - requires a completed checkout', responses: {'200': {description: Portal URL}, '422': {description: No billing profile yet}}}
  /v1/billing/checkout:
    post: {summary: 'Start checkout {plan_id, currency, interval month|year}: returns hosted payment URL; an active Stripe subscriber gets an in-place prorated plan change instead (status plan_changed)', responses: {'200': {description: URL, trial_activated or plan_changed}, '422': {description: Invalid plan/currency/interval or currency change on existing subscription}, '502': {description: Gateway unavailable}}}
  /v1/tenant:
    patch: {summary: 'Owner only - rename the organization {name} and/or set the default sending identity {from_name, from_email on a verified domain}; omitted fields untouched', responses: {'204': {description: Updated}, '422': {description: Invalid name/email or domain not verified}}}
  /v1/forms:
    get: {summary: List hosted form endpoints, responses: {'200': {description: Endpoints with public action_url}}}
    post: {summary: 'Create a form endpoint {name, to_email, from_email (verified domain), redirect_url?, allowed_origins?}', responses: {'201': {description: Endpoint + action_url}, '422': {description: Invalid input}}}
  /v1/forms/{id}:
    patch: {summary: Enable or disable a form endpoint, parameters: [{in: path, name: id, required: true, schema: {type: string}}], responses: {'204': {description: Updated}, '404': {description: Not found}}}
    delete: {summary: Delete a form endpoint, parameters: [{in: path, name: id, required: true, schema: {type: string}}], responses: {'204': {description: Deleted}, '404': {description: Not found}}}
  /f/{token}:
    post: {security: [], summary: 'Public form submission (HTML form post, JSON or multipart) - honeypot field "website", per-endpoint rate limit, optional origin allowlist; responds with redirect, thank-you page or {ok:true}', parameters: [{in: path, name: token, required: true, schema: {type: string}}], responses: {'200': {description: Accepted}, '303': {description: Redirect}, '403': {description: Origin not allowed}, '429': {description: Rate limited}}}
  /v1/webhooks/stripe:
    post: {security: [], summary: Stripe events (signature-verified, deduplicated), responses: {'200': {description: Processed}}}
  /v1/webhooks/razorpay:
    post: {security: [], summary: Razorpay events (signature-verified, deduplicated), responses: {'200': {description: Processed}}}
  /v1/admin/plans:
    get: {summary: All plans (platform admin), responses: {'200': {description: Plans}}}
  /v1/admin/plans/{id}:
    patch:
      summary: Edit a plan's prices, limits or availability
      parameters: [{in: path, name: id, required: true, schema: {type: string}}]
      responses: {'204': {description: Updated}, '404': {description: Not found}}
  /v1/auth/2fa:
    post: {security: [], summary: 'Finish a two-factor sign-in (challenge token + 6-digit code)', responses: {'200': {description: Authenticated}, '401': {description: Challenge or code invalid}}}
  /v1/me/2fa:
    get: {summary: Whether two-factor is enabled for the current user, responses: {'200': {description: Status}}}
  /v1/me/2fa/setup:
    post: {summary: Generate a TOTP secret (returns base32 secret + otpauth URI), responses: {'200': {description: Secret}, '409': {description: Already enabled}}}
  /v1/me/2fa/enable:
    post: {summary: Verify a code and turn two-factor on, responses: {'200': {description: Enabled}, '422': {description: Invalid code}}}
  /v1/me/2fa/disable:
    post: {summary: Verify a code and turn two-factor off, responses: {'200': {description: Disabled}, '422': {description: Invalid code}}}
  /v1/team:
    get: {summary: List team members (owner/admin), responses: {'200': {description: Members}}}
  /v1/team/invite:
    post: {summary: 'Email a 7-day invitation link (role: owner/admin/developer/reports). With expiresHours>0 the member is created immediately as TEMPORARY guest/developer access that locks itself when the time passes (sessions and sign-in links stop working)', responses: {'201': {description: Invited}, '409': {description: User already exists}, '422': {description: Invalid input / temporary access cannot be owner}}}
  /v1/team/{id}:
    patch:
      summary: Change a member's role or disable them (guards against removing the last owner or editing yourself); extendHours>0 extends or reactivates temporary access from now and emails a fresh sign-in link; sendLink:true just re-sends a link
      parameters: [{in: path, name: id, required: true, schema: {type: string}}]
      responses: {'204': {description: Updated}, '404': {description: Not found}, '422': {description: Guard refused}}
  /v1/admin/tenants/{id}:
    get:
      summary: Full customer detail - profile, usage, domains, limits, recent emails (platform admin)
      parameters: [{in: path, name: id, required: true, schema: {type: string}}]
      responses: {'200': {description: Detail}, '404': {description: Not found}}
  /v1/admin/tenants/{id}/impersonate:
    post:
      summary: Platform admin logs in as the customer's owner - 1-hour session, replaces the caller's session cookie, audit-logged (CHANGE F-132)
      parameters: [{in: path, name: id, required: true, schema: {type: string}}]
      responses: {'200': {description: Session switched}, '404': {description: No active owner for tenant}}
  /v1/templates:
    get: {summary: List email templates, responses: {'200': {description: Templates}}}
    post: {summary: 'Create a template ({{key}} placeholders in subject/text/html)', responses: {'201': {description: Created}, '409': {description: Name already exists}}}
  /v1/templates/{id}:
    patch:
      summary: Update a template
      parameters: [{in: path, name: id, required: true, schema: {type: string}}]
      responses: {'204': {description: Updated}, '404': {description: Not found or name exists}}
    delete:
      summary: Delete a template
      parameters: [{in: path, name: id, required: true, schema: {type: string}}]
      responses: {'204': {description: Deleted}, '404': {description: Not found}}
  /v1/domains:
    get: {summary: List sending domains, responses: {'200': {description: Domains}}}
    post: {summary: Add a sending domain, responses: {'201': {description: DNS verification challenge}}}
  /v1/domains/{id}/verify:
    post:
      summary: Check DNS ownership
      parameters: [{in: path, name: id, required: true, schema: {type: string}}]
      responses: {'200': {description: Verification result}}
  /v1/email/{id}/eml:
    get:
      summary: Download a logged email as a standard .eml file (full headers, text/HTML parts, attachments)
      parameters: [{in: path, name: id, required: true, schema: {type: string}}]
      responses: {'200': {description: message/rfc822 attachment}, '404': {description: Not found}}
  /v1/credentials:
    get: {summary: List masked API and SMTP credentials, responses: {'200': {description: Credentials}}}
    post: {summary: Create a credential with one-time secret display (SMTP users may pass their own username), responses: {'201': {description: Credential}, '409': {description: Username taken}, '422': {description: Invalid input}}}
  /v1/credentials/{id}:
    patch:
      summary: Edit a credential - rename, allow/block, set or clear a per-credential rate limit, pin to allowed IPs/CIDRs (allowedIps, blank = anywhere), restrict From addresses (allowedSenders - emails or domains, blank = any verified domain)
      parameters: [{in: path, name: id, required: true, schema: {type: string}}]
      responses: {'204': {description: Updated}, '404': {description: Not found or revoked}, '422': {description: Invalid input}}
    delete:
      summary: Revoke a credential
      parameters: [{in: path, name: id, required: true, schema: {type: string}}]
      responses: {'204': {description: Revoked}}
  /v1/credentials/{id}/password:
    get:
      summary: Reveal an SMTP user's current password (owner/admin, audit-logged)
      parameters: [{in: path, name: id, required: true, schema: {type: string}}]
      responses: {'200': {description: Username and password}, '404': {description: Not found, not an SMTP user, or password not stored}}
    post:
      summary: Change an SMTP user's password - caller's own (12+ chars) or blank body for a fresh random one, returned once
      parameters: [{in: path, name: id, required: true, schema: {type: string}}]
      responses: {'200': {description: New password}, '404': {description: Not found or not an SMTP user}, '422': {description: Password too short}}
  /v1/suppressions:
    get:
      summary: List suppressions, filterable
      parameters:
        - {in: query, name: q, schema: {type: string}, description: Email contains}
        - {in: query, name: reason, schema: {type: string, enum: [hard_bounce, complaint, unsubscribe, manual]}}
      responses: {'200': {description: Suppressions}}
    post: {summary: Suppress a recipient, responses: {'201': {description: Suppression}}}
  /v1/suppressions/{id}:
    delete:
      summary: Remove a manual suppression
      parameters: [{in: path, name: id, required: true, schema: {type: string}}]
      responses: {'204': {description: Removed}}
  /v1/webhooks:
    get: {summary: List customer webhook endpoints, responses: {'200': {description: Webhooks}}}
    post: {summary: Create a signed customer webhook endpoint, responses: {'201': {description: Endpoint and one-time signing secret}}}
  /v1/webhooks/{id}:
    patch: {summary: 'Edit a webhook endpoint - url, events, status active|disabled (disabling resets the failure counter)', parameters: [{in: path, name: id, required: true, schema: {type: string}}], responses: {'204': {description: Updated}, '404': {description: Not found}, '422': {description: Invalid input}}}
    delete: {summary: Delete a webhook endpoint, parameters: [{in: path, name: id, required: true, schema: {type: string}}], responses: {'204': {description: Deleted}, '404': {description: Not found}}}
  /v1/admin/tenants:
    get: {summary: List customers, responses: {'200': {description: Tenants}, '403': {description: Platform admin required}}}
    post: {summary: Create a customer and owner, responses: {'201': {description: Tenant}}}
  /v1/admin/tenants/{id}:
    patch:
      summary: Update customer status, plan or limits
      parameters: [{in: path, name: id, required: true, schema: {type: string}}]
      responses: {'204': {description: Updated}}
  /v1/admin/settings:
    get: {summary: Read masked central configuration, responses: {'200': {description: Settings}}}
    put: {summary: Save central configuration, responses: {'204': {description: Saved}}}
  /v1/admin/providers:
    get: {summary: List delivery providers and current health, responses: {'200': {description: Providers}}}
    post: {summary: Connect SMTP2GO, SendGrid, Mailgun, SMTP or custom JSON provider, responses: {'201': {description: Provider created}}}
  /v1/admin/providers/{id}:
    patch:
      summary: Update provider settings, credentials or status
      parameters: [{in: path, name: id, required: true, schema: {type: string}}]
      responses: {'204': {description: Updated}}
    delete:
      summary: Delete an unused delivery provider
      parameters: [{in: path, name: id, required: true, schema: {type: string}}]
      responses: {'204': {description: Deleted}, '409': {description: Provider is used by a route}}
  /v1/admin/providers/{id}/test:
    post:
      summary: Send a provider connection test
      parameters: [{in: path, name: id, required: true, schema: {type: string}}]
      responses: {'200': {description: Provider accepted test}, '502': {description: Provider test failed}}
  /v1/admin/providers/{id}/attempts:
    get:
      summary: List provider delivery attempts, responses and latency
      parameters: [{in: path, name: id, required: true, schema: {type: string}}]
      responses: {'200': {description: Delivery attempts}}
  /v1/admin/routes:
    get: {summary: List platform, tenant and domain routing policies, responses: {'200': {description: Routes}}}
    post: {summary: Create failover, weighted or round-robin route, responses: {'201': {description: Route created}}}
  /v1/admin/routes/{id}:
    put:
      summary: Replace a routing policy and provider pool
      parameters: [{in: path, name: id, required: true, schema: {type: string}}]
      responses: {'204': {description: Updated}}
    delete:
      summary: Delete a routing policy
      parameters: [{in: path, name: id, required: true, schema: {type: string}}]
      responses: {'204': {description: Deleted}}
  /v1/webhooks/providers/{id}:
    post:
      security: []
      summary: Receive a normalized signed event from a delivery-provider adapter
      parameters: [{in: path, name: id, required: true, schema: {type: string}}]
      responses: {'204': {description: Event accepted}, '401': {description: Invalid provider signature}}
components:
  securitySchemes:
    bearerAuth: {type: http, scheme: bearer}
  schemas:
    SendEmail:
      type: object
      required: [from, to, subject]
      properties:
        from: {type: string, format: email}
        to: {type: array, minItems: 1, maxItems: 100, items: {type: string, format: email}}
        subject: {type: string}
        text: {type: string}
        html: {type: string}
        headers: {type: object, additionalProperties: {type: string}}
        metadata: {type: object, additionalProperties: {type: string}}
        template_id: {type: string, description: 'Fill empty subject/text/html from this template'}
        substitutions: {type: object, additionalProperties: {type: string}, description: 'Values for {{key}} placeholders (HTML-escaped in the HTML body)'}
        attachments:
          type: array
          maxItems: 10
          description: 'Files to attach; decoded total is capped by sending.max_message_bytes'
          items:
            type: object
            required: [filename, content]
            properties:
              filename: {type: string}
              content_type: {type: string, default: application/octet-stream}
              content: {type: string, format: base64}
