> ## Documentation Index
> Fetch the complete documentation index at: https://docs.papermap.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Creating Dashboards

This guide covers how to create dashboards for your tenants using the Papermap Dashboard API.

<Info>
  The `dashboardId` created through this flow can be used in both iframe
  embedding and React component embedding with
  [`@papermap/papermap`](/documentation/embedding/react-components).
</Info>

## Prerequisites

Before implementing, ensure you have:

* Papermap API credentials (API Key ID and Secret Key)
* Your workspace ID
* API endpoint URL (obtain from your Papermap dashboard settings)
* HMAC authentication implemented (see [Authentication](/documentation/multi-tenancy/authentication))

## Database Model

First, you should have created a table in your database to store the mapping between your tenant and the
dashboard ID. See the [Database Model section in Backend
Implementation](/documentation/multi-tenancy/backend#database-model) for details.

**Purpose**: Links your tenant to a Papermap dashboard ID for secure access and isolation.

## Creating a Dashboard

<Info>
  The base URL for the Papermap API is `https://prod.dataapi.papermap.ai/`.
</Info>

<Info>
  The `get_auth_headers` function is a helper function that generates the HMAC signature for the API request found
  in the [HMAC Authentication](/documentation/multi-tenancy/authentication)
</Info>

Create a dashboard via the Papermap API for each tenant:

<CodeGroup>
  ```python Python theme={null}
  import httpx

  async def create_dashboard(
      tenant_id: str,
      workspace_id: str,
      name: str,
      description: Optional[str] = None,
      created_by: Optional[str] = None
  ):
      # 1. Call Papermap API with HMAC signature
      headers = _get_auth_headers(workspace_id, API_KEY_ID, SECRET_KEY)

      payload = {
          "workspace_id": workspace_id,
          "title": name,
          "description": description
      }

      async with httpx.AsyncClient() as client:
          response = await client.post(
              f"{PAPERMAP_API_URL}/api/v1/external/dashboards",
              headers=headers,
              json=payload
          )

      external_dashboard = response.json()
      dashboard_id = external_dashboard["data"]["data"]["dashboard_id"]

      # 2. Store mapping in your database
      tenant_dashboard = TenantDashboard.create(
          session=db,
          tenant_id=tenant_id,
          workspace_id=workspace_id,
          dashboard_id=dashboard_id,
          created_by=created_by
      )

      return tenant_dashboard
  ```

  ```typescript TypeScript/Node.js theme={null}
  import axios from "axios";

  async function createDashboard(
    tenantId: string,
    workspaceId: string,
    name: string,
    description?: string
  ) {
    const service = new DashboardAPIService({
      apiKeyId: process.env.PAPERMAP_API_KEY_ID,
      secretKey: process.env.PAPERMAP_SECRET_KEY,
      workspaceId: workspaceId,
      baseUrl: process.env.PAPERMAP_API_URL,
    });

    const headers = service.getAuthHeaders();

    const response = await axios.post(
      `${service.config.baseUrl}/api/v1/external/dashboards`,
      {
        workspace_id: workspaceId,
        title: name,
        description,
      },
      { headers }
    );

    const dashboardId = response.data.data.dashboard_id;

    // Store mapping in your database
    await db.tenantDashboards.create({
      tenant_id: tenantId,
      workspace_id: workspaceId,
      dashboard_id: dashboardId,
    });

    return { dashboardId, ...response.data };
  }
  ```

  ```php PHP theme={null}
  <?php
  function createDashboardForTenant($tenantId, $workspaceId, $name, $description = null) {
      $headers = getAuthHeaders(
          $workspaceId,
          getenv('PAPERMAP_API_KEY_ID'),
          getenv('PAPERMAP_SECRET_KEY')
      );

      $payload = [
          'workspace_id' => $workspaceId,
          'title' => $name,
          'description' => $description
      ];

      $ch = curl_init(getenv('PAPERMAP_API_URL') . '/api/v1/external/dashboards');
      curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
      curl_setopt($ch, CURLOPT_POST, true);
      curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));

      $headerArray = [];
      foreach ($headers as $key => $value) {
          $headerArray[] = "$key: $value";
      }
      curl_setopt($ch, CURLOPT_HTTPHEADER, $headerArray);

      $response = curl_exec($ch);
      curl_close($ch);

      $data = json_decode($response, true);
      $dashboardId = $data['data']['data']['id'];

      // Store mapping in your database
      DB::table('tenant_dashboards')->insert([
          'tenant_id' => $tenantId,
          'workspace_id' => $workspaceId,
          'dashboard_id' => $dashboardId
      ]);

      return ['dashboard_id' => $dashboardId, 'data' => $data];
  }
  ?>
  ```

  ```java Java theme={null}
  public DashboardResponse createDashboard(
      String workspaceId,
      String name,
      String description
  ) throws Exception {
      HttpHeaders headers = getAuthHeaders(workspaceId);

      Map<String, String> payload = new HashMap<>();
      payload.put("workspace_id", workspaceId);
      payload.put("title", name);
      payload.put("description", description);

      HttpEntity<Map<String, String>> request = new HttpEntity<>(payload, headers);

      ResponseEntity<DashboardResponse> response = restTemplate.exchange(
          baseUrl + "/api/v1/external/dashboards",
          HttpMethod.POST,
          request,
          DashboardResponse.class
      );

      return response.getBody();
  }
  ```

  ```csharp C# theme={null}
  public async Task<DashboardResponse> CreateDashboard(
      string workspaceId,
      string name,
      string description = null
  )
  {
      var headers = GetAuthHeaders(workspaceId);
      var request = new HttpRequestMessage(
          HttpMethod.Post,
          $"{_baseUrl}/api/v1/external/dashboards"
      );

      foreach (var header in headers)
      {
          request.Headers.Add(header.Key, header.Value);
      }

      var payload = new
      {
          workspace_id = workspaceId,
          title = name,
          description
      };

      request.Content = new StringContent(
          JsonSerializer.Serialize(payload),
          Encoding.UTF8,
          "application/json"
      );

      var response = await _httpClient.SendAsync(request);
      var content = await response.Content.ReadAsStringAsync();

      return JsonSerializer.Deserialize<DashboardResponse>(content);
  }
  ```

  ```go Go theme={null}
  func (s *DashboardAPIService) CreateDashboard(
      workspaceID, name, description string,
  ) (*Dashboard, error) {
      headers := s.getAuthHeaders(workspaceID)

      payload := map[string]string{
          "workspace_id": workspaceID,
          "title":        name,
          "description":  description,
      }

      jsonData, _ := json.Marshal(payload)
      req, _ := http.NewRequest(
          "POST",
          s.BaseURL+"/api/v1/external/dashboards",
          strings.NewReader(string(jsonData)),
      )

      for key, value := range headers {
          req.Header.Set(key, value)
      }

      resp, err := s.Client.Do(req)
      if err != nil {
          return nil, err
      }
      defer resp.Body.Close()

      var result Dashboard
      json.NewDecoder(resp.Body).Decode(&result)

      return &result, nil
  }
  ```
</CodeGroup>

## Workflow Diagram

```
1. Client Request → POST /api/v1/dashboards
   {
     "tenant_id": "org-123",
     "workspace_id": "workspace-456",
     "name": "Sales Dashboard"
   }

2. Your API → Generate HMAC Signature
   payload = "workspace-456" + "1699999999"
   signature = HMAC-SHA256(payload, secret_key)

3. Your API → Call Papermap Dashboard API
   POST ${PAPERMAP_API_URL}/api/v1/external/dashboards
   Headers:
     X-API-Key-ID: your-api-key
     X-Workspace-ID: workspace-456
     X-Valid-Until: 1699999999
     X-Signature: abc123...

4. Papermap API → Returns Dashboard
   {data:... {data: {dashboard_id: "dashboard-789", ...}}}

5. Your API → Store Mapping in Database
   TenantDashboard:
     tenant_id: org-123
     workspace_id: workspace-456
     dashboard_id: dashboard-789

6. Return to Client
   { "success": true, "data": {...} }
```

## Key Considerations

### Tenant Isolation

Each tenant should have their own dashboard:

* One dashboard per tenant ensures data isolation
* Store the tenant-to-dashboard mapping in your database
* Verify tenant ownership before allowing access

### Dashboard Naming

Use clear, consistent naming conventions:

* Include tenant identifier in dashboard name
* Example: `"Dashboard - Acme Corp"` or `"Acme Corp Analytics"`
* Makes management easier in the Papermap admin panel

### Error Handling

Handle common errors gracefully:

```python theme={null}
try:
    dashboard = await create_dashboard(tenant_id, workspace_id, name)
except httpx.HTTPStatusError as e:
    if e.response.status_code == 401:
        # Invalid credentials or expired signature
        raise Exception("Authentication failed")
    elif e.response.status_code == 409:
        # Dashboard already exists
        raise Exception("Dashboard already exists for this tenant")
    else:
        # Other errors
        raise Exception(f"Failed to create dashboard: {e}")
```

## Next Steps

<CardGroup cols={2}>
  <Card title="Generate Iframe Tokens" icon="code" href="/documentation/multi-tenancy/iframe-tokens">
    Learn how to generate secure embed tokens for your dashboards
  </Card>

  <Card title="API Endpoints" icon="plug" href="/documentation/multi-tenancy/api-endpoints">
    Set up REST API endpoints for dashboard operations
  </Card>
</CardGroup>
