Skip to main content

Overview

InstaView uses scope-based permissions to provide granular access control. Each API key has a set of scopes that determine which operations it can perform on which resources.

Permission Model

Three-Level Access Control

Every resource type supports three permission levels:

Read

GET operations - List resources - View resource details - Search and filter

Write

POST and PATCH operations - Create resources - Update resources - Modify configurations

Delete

DELETE operations - Soft delete resources - Remove associations - Archive data

Scope Format

Scopes follow the format: <permission>:<resource>
read:jobs       // Can list and view jobs
write:candidates // Can create and update candidates
delete:agents    // Can delete agents

Available Scopes

ScopePermissionsOperations
read:jobsView jobsGET /jobs, GET /jobs/{id}
write:jobsCreate/update jobsPOST /jobs, PATCH /jobs/{id}
delete:jobsDelete jobsDELETE /jobs/{id}
Common Combinations:
["read:jobs"]                          // Read-only
["read:jobs", "write:jobs"]            // Create and read
["read:jobs", "write:jobs", "delete:jobs"] // Full access

Scope Validation

Request-Time Validation

Every API request validates the required scopes:
// Request
POST /jobs
Authorization: Bearer sk_xxx (scopes: ["read:jobs"])

// Response: 403 Forbidden
{
  "message": "Insufficient permissions: Required scope write:jobs not found",
  "error": "Forbidden",
  "statusCode": 403
}

Validation Flow

1

Extract API Key

API key is extracted from Authorization: Bearer header
2

Validate Key

Key is validated (not revoked, not expired, not suspended)
3

Check Scopes

Request’s required scope is compared against key’s scopes
4

Company Isolation

Verify resource belongs to key’s company (if applicable)
5

Execute Request

If all checks pass, request is processed

Scope Strategy

By Use Case

{
  "name": "Analytics Dashboard",
  "scopes": [
    "read:jobs",
    "read:candidates",
    "read:interviews",
    "read:billing"
  ]
}
Rationale: Dashboard only needs to view data, never modify it.

Least Privilege Principle

Always grant the minimum scopes needed:
  • Reduces Attack Surface: Compromised keys have limited damage potential
  • Prevents Accidents: Applications can’t accidentally delete data they shouldn’t
  • Audit Trail: Scope requirements make it clear what each integration does
  • Compliance: Many regulations require least-privilege access
A webhook handler that processes interview completion events: ❌ Too many scopes: json ["read:interviews", "write:interviews", "read:candidates", "write:candidates"] Minimal scopes: json ["read:interviews", "read:candidates"] The webhook only needs to read interview data, not create or modify it.
A reporting tool that generates analytics:Includes write scopes:
["read:jobs", "write:jobs", "read:candidates", "write:candidates"]
Read-only:
["read:jobs", "read:candidates", "read:interviews", "read:billing"]
Reporting tools should never have write or delete permissions.

Scope Dependencies

Some operations require multiple scopes due to resource relationships:

Creating Candidates

POST /candidates
{
  "jobId": "job-uuid",
  "firstName": "Jane",
  "lastName": "Doe"
}
Required Scopes:
  • write:candidates - To create the candidate
  • read:jobs - To validate the job exists and belongs to your company

Creating Interviews

POST /interviews
{
  "candidateId": "candidate-uuid",
  "agentId": "agent-uuid"
}
Required Scopes:
  • write:interviews - To create the interview
  • read:candidates - To validate candidate ownership
  • read:agents - To validate agent ownership

Listing Candidates with Job Filters

GET /candidates?jobId=job-uuid
Required Scopes:
  • read:candidates - To list candidates
  • read:jobs - To filter by job

Cross-Resource Operations

Inline Resources

Some operations allow creating inline resources:
// Create interview with inline candidate
POST /interviews
{
  "candidate": {
    "jobId": "job-uuid",
    "firstName": "Jane",
    "lastName": "Doe",
    "email": "[email protected]",
    "gdprExpiryDate": "2026-11-16T12:00:00Z"
  },
  "agentId": "agent-uuid"
}
Required Scopes:
  • write:interviews - Primary operation
  • write:candidates - To create inline candidate
  • read:agents - To validate agent
  • read:jobs - To validate job
Inline resources (candidate, job, agent) are created automatically as part of the parent operation and follow the same scope requirements. The XOR behavior ensures you use either existing resource IDs or inline resource definitions, not both.

Company Isolation

All scopes are enforced within company boundaries:

Regular API Keys

// Key belongs to Company A
Authorization: Bearer sk_abc123def456ghi789

// ✅ Can access Company A's jobs
GET /jobs

// ❌ Cannot access Company B's jobs (even if you know the ID)
GET /jobs/company-b-job-uuid
// Returns: 403 Forbidden

ATS Integration Keys

// ATS key with access to multiple companies
Authorization: Bearer sk_ats123xyz456abc789

// ✅ Must specify companyId for Company A
GET /jobs?companyId=company-a-uuid

// ✅ Can access Company B with different companyId
GET /jobs?companyId=company-b-uuid

// ❌ Cannot access Company C (not managed by this ATS key)
GET /jobs?companyId=company-c-uuid
// Returns: 403 Forbidden - "Access denied to this company"

Scope Errors

Insufficient Permissions (403)
error
The API key lacks the required scope
{
  "message": "Insufficient permissions: Required scope write:jobs not found",
  "error": "Forbidden",
  "statusCode": 403
}
Solution: Add the required scope to your API key or create a new key with appropriate scopes.
Access Denied (403)
error
Resource doesn’t exist or belongs to another company
{
  "message": "Access denied to this resource",
  "error": "Forbidden",
  "statusCode": 403
}
Causes:
  • Resource belongs to a different company
  • Resource has been soft-deleted
  • Invalid resource ID

Best Practices

Instead of one key with all scopes, create multiple keys for different purposes:
// Separate keys for different integrations
const analyticsKey = 'sk_live_analytics';  // read-only
const syncKey = 'sk_live_sync';            // write candidates
const automationKey = 'sk_live_automation'; // write interviews
Document which scopes each part of your application needs: ```javascript /** * Candidate Sync Service * * Required Scopes: * - read:jobs (validate job ownership) * - read:candidates (check for duplicates) * - write:candidates (create/update candidates) */ class CandidateSyncService
</Accordion>

<Accordion title="Handle Permission Errors" icon="triangle-exclamation">
  Gracefully handle insufficient permission errors:
  
  ```javascript
  async function createJob(jobData) {
    try {
      return await api.post('/jobs', jobData);
    } catch (error) {
      if (error.response?.status === 403) {
        console.error(
          `Permission denied: ${error.response.data.message}`
        );
        // Notify admin or fallback to alternative workflow
      }
      throw error;
    }
  }
Periodically review API key scopes:
  • Are all scopes still necessary?
  • Can any keys be downgraded to read-only?
  • Are there unused keys that can be revoked?

Testing Scopes

Verify Your Scopes

// Verify your API key by making a simple GET request
// to any read endpoint you have access to, such as:
GET /jobs
Authorization: Bearer sk_abc123def456ghi789

// If successful, your API key is valid and has the required scope

Test Scope Requirements

// Test each operation with your key
const tests = [
  { method: "GET", path: "/jobs", scope: "read:jobs" },
  { method: "POST", path: "/jobs", scope: "write:jobs" },
  { method: "GET", path: "/candidates", scope: "read:candidates" },
  { method: "POST", path: "/candidates", scope: "write:candidates" },
];

for (const test of tests) {
  try {
    await api.request(test.method, test.path);
    console.log(`✅ ${test.scope}: Success`);
  } catch (error) {
    if (error.response?.status === 403) {
      console.log(`❌ ${test.scope}: Missing`);
    }
  }
}

Next Steps