Docs Customer Portal

Customer Portal

The Customer Portal provides a self-service interface where your customers can submit tickets, track their existing requests, reply to agents, and search your knowledge base. Authentication uses a passwordless magic link flow.

Authentication Flow

The portal uses a two-step magic link authentication:

  1. Customer requests a magic link by providing their email address
  2. Tiqora sends a one-time login link to that email (valid for 15 minutes)
  3. Customer clicks the link, which sends the token to the verify endpoint
  4. Tiqora returns a 7-day JWT for subsequent portal API calls
POST /api/v1/portal/auth/magic-link

Request Body

Field Type Required Description
email string Yes Customer's email address

Example Request

{
  "email": "jane@example.com"
}

Example Response (200 OK)

{
  "data": {
    "message": "If an account exists for this email, a login link has been sent.",
    "expires_in": 900
  }
}

Important: The response is intentionally vague to prevent email enumeration. A 200 is returned whether or not the email exists in the system.

Step 2: Verify the Token

POST /api/v1/portal/auth/verify

The magic link email contains a URL with a token parameter. Your portal frontend extracts this token and sends it to the verify endpoint.

Request Body

Field Type Required Description
token string Yes The magic link token from the email

Example Request

{
  "token": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0"
}

Example Response (200 OK)

{
  "data": {
    "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
    "token_type": "Bearer",
    "expires_in": 604800,
    "customer": {
      "id": "01912345-0000-7abc-def0-123456789abc",
      "email": "jane@example.com",
      "name": "Jane Smith"
    }
  }
}

Error: Invalid or Expired Token

{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "The login link is invalid or has expired. Please request a new one."
  }
}

Step 3: Use the Portal JWT

Include the JWT in all subsequent portal requests:

Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...

The portal JWT is distinct from the dashboard JWT. It contains:

Claim Description
sub Customer ID (UUID)
tid Tenant ID (UUID)
type portal (distinguishes from agent tokens)
exp Expiry timestamp (7 days from issuance)

Get Current Customer

GET /api/v1/portal/auth/me

Returns the authenticated customer's profile.

Example Response

{
  "data": {
    "id": "01912345-0000-7abc-def0-123456789abc",
    "email": "jane@example.com",
    "name": "Jane Smith",
    "phone": "+1 555-123-4567",
    "company": "Acme Corp",
    "created_at": "2024-11-01T12:00:00Z"
  }
}

List Customer Tickets

GET /api/v1/portal/tickets

Returns tickets belonging to the authenticated customer. Only the customer's own tickets are visible.

Query Parameters

Parameter Type Default Description
per_page integer 25 Results per page (max: 100)
cursor string Cursor for next page
status string Filter by status

Example Response

{
  "data": [
    {
      "id": "01912345-6789-7abc-def0-123456789abc",
      "ticket_number": "1043",
      "subject": "Payment not processing",
      "status": "in_progress",
      "priority": "high",
      "created_at": "2025-01-15T10:00:00Z",
      "updated_at": "2025-01-15T10:30:00Z",
      "last_reply_at": "2025-01-15T10:25:00Z"
    },
    {
      "id": "01912345-5555-7abc-def0-123456789abc",
      "ticket_number": "1038",
      "subject": "How to export data?",
      "status": "resolved",
      "priority": "medium",
      "created_at": "2025-01-12T14:00:00Z",
      "updated_at": "2025-01-13T09:00:00Z",
      "last_reply_at": "2025-01-13T09:00:00Z"
    }
  ],
  "meta": {
    "per_page": 25,
    "has_more": false,
    "next_cursor": null
  }
}

Get a Ticket

GET /api/v1/portal/tickets/{ticket_id}

Returns a single ticket with its conversation history. Internal notes are automatically excluded from the response.

Example Response

{
  "data": {
    "id": "01912345-6789-7abc-def0-123456789abc",
    "ticket_number": "1043",
    "subject": "Payment not processing",
    "description": "I'm trying to upgrade my plan but the payment keeps failing.",
    "status": "in_progress",
    "priority": "high",
    "created_at": "2025-01-15T10:00:00Z",
    "updated_at": "2025-01-15T10:30:00Z",
    "replies": [
      {
        "id": "01912345-aaaa-7abc-def0-123456789abc",
        "body": "Hi Jane, I can see the issue with your payment. Let me look into this for you.",
        "author_type": "agent",
        "author_name": "Alex Johnson",
        "attachments": [],
        "created_at": "2025-01-15T10:15:00Z"
      }
    ]
  }
}

Important: Internal notes (is_internal_note: true) are never included in portal responses. Customers only see public replies from agents, other customer replies, and system messages.

Create a Ticket

POST /api/v1/portal/tickets

Request Body

Field Type Required Description
subject string Yes Ticket subject (max 500 chars)
description string Yes Ticket description, Markdown supported (min 20 chars)
priority string No low, medium, high. Default: medium
category_id uuid No Category for the ticket

Example Request

{
  "subject": "Cannot download invoices",
  "description": "When I click the download button on my invoices page, nothing happens. I've tried multiple browsers (Chrome and Firefox) on macOS. The issue started this morning.",
  "priority": "medium"
}

Example Response (201 Created)

{
  "data": {
    "id": "01912345-7777-7abc-def0-123456789abc",
    "ticket_number": "1044",
    "subject": "Cannot download invoices",
    "status": "new",
    "priority": "medium",
    "created_at": "2025-01-16T09:00:00Z"
  }
}

Hint: Portal customers cannot set urgent priority. If the issue is truly urgent, an agent can escalate it from the dashboard.

Reply to a Ticket

POST /api/v1/portal/tickets/{ticket_id}/replies

Request Body

Field Type Required Description
body string Yes Reply content, Markdown supported

Example Request

{
  "body": "Thanks Alex! The payment went through now. However, I notice my plan still shows as **Basic**. Could you check if the upgrade was applied?"
}

Example Response (201 Created)

{
  "data": {
    "id": "01912345-cccc-7abc-def0-123456789abc",
    "ticket_id": "01912345-6789-7abc-def0-123456789abc",
    "body": "Thanks Alex! The payment went through now...",
    "author_type": "customer",
    "author_name": "Jane Smith",
    "attachments": [],
    "created_at": "2025-01-15T10:35:00Z"
  }
}

Hint: When a customer replies to a ticket in waiting_on_customer status, the ticket automatically transitions back to in_progress.

Search Knowledge Base

GET /api/v1/portal/kb/search

Search published knowledge base articles. Uses full-text and semantic search to return the most relevant results.

Query Parameters

Parameter Type Required Description
q string Yes Search query (min 3 chars)
per_page integer No Results per page (max: 20). Default: 10
category_id uuid No Filter by KB category

Example Request

GET /api/v1/portal/kb/search?q=reset+password&per_page=5

Example Response

{
  "data": [
    {
      "id": "01912345-8888-7abc-def0-123456789abc",
      "title": "How to Reset Your Password",
      "excerpt": "If you've forgotten your password, you can reset it in a few simple steps. Go to the login page and click 'Forgot Password'...",
      "category": "Account Management",
      "relevance_score": 0.94,
      "updated_at": "2025-01-10T12:00:00Z"
    },
    {
      "id": "01912345-9999-7abc-def0-123456789abc",
      "title": "Two-Factor Authentication Setup",
      "excerpt": "Secure your account with two-factor authentication. After setting a password, you can enable 2FA from your security settings...",
      "category": "Account Management",
      "relevance_score": 0.71,
      "updated_at": "2025-01-08T15:00:00Z"
    }
  ],
  "meta": {
    "total": 2,
    "per_page": 5
  }
}

Hint: Only articles with visibility: public are returned in portal search results. Draft and internal articles are excluded.

Portal Rate Limits

Portal endpoints have their own rate limits separate from the main API:

Endpoint Limit
POST /portal/auth/magic-link 3 requests per 15 minutes per email
POST /portal/auth/verify 5 attempts per 15 minutes per IP
All authenticated endpoints 60 requests per minute per customer

Best Practices

  1. Handle token expiry gracefully — The portal JWT lasts 7 days. When it expires, redirect the customer to request a new magic link rather than showing a raw error.

  2. Show KB results before ticket creation — When a customer starts creating a ticket, search the knowledge base for relevant articles. This can deflect tickets and reduce support load.

  3. Hide internal details — The portal API automatically filters internal notes and agent-only metadata. Do not expose raw dashboard API responses to customers.

  4. Use the magic link response message as-is — The vague "if an account exists" message is intentional. Do not add logic that reveals whether an email exists in your system.