Skip to main content
The Penbox API provides two powerful filtering methods to query and retrieve exactly the data you need. Both methods are available on GET (list) and GET /count endpoints.

Two Filtering Methods

Method 1: Direct Query Parameters

The simplest way to filter is by adding query parameters directly to the URL:
GET /v1/cases?workspace[id]=abc-123&owner[email]=[email protected]&status=pending
Benefits:
  • Simple and readable
  • Easy to construct manually
  • Good for basic filtering needs
Example:
// Filter cases by workspace and owner
const params = new URLSearchParams({
  'workspace[id]': workspaceId,
  'owner[email]': '[email protected]',
  'archived': 'false'
});

const response = await fetch(
  `https://connect.penbox.io/v1/cases?${params}`,
  { headers: { 'Authorization': `Bearer ${token}` } }
);

Method 2: Advanced JSON Filter

For complex queries, use the filter parameter with a JSON object containing operators:
GET /v1/cases?filter={"attributes":{"created_at":{"$gt":"2024-01-01"}}}
Benefits:
  • Powerful operator-based filtering
  • Support for complex conditions
  • Filter on attributes and relationships
  • Combine multiple criteria with operators
Example:
const filter = {
  attributes: {
    created_at: { $gt: '2024-01-01T00:00:00Z' },
    status: { $ne: 'draft' }
  },
  relationships: {
    owner: {
      attributes: {
        email: { $eq: '[email protected]' }
      }
    }
  }
};

const response = await fetch(
  `https://connect.penbox.io/v1/cases?filter=${encodeURIComponent(JSON.stringify(filter))}`,
  { headers: { 'Authorization': `Bearer ${token}` } }
);

Filter Operators

When using the JSON filter method, you can use the following operators:
OperatorDescriptionExample
$eqEquals{ status: { $eq: 'pending' } }
$neNot equals{ status: { $ne: 'draft' } }
$gtGreater than{ created_at: { $gt: '2024-01-01' } }
$ltLess than{ created_at: { $lt: '2024-12-31' } }
$geGreater than or equal to{ created_at: { $ge: '2024-01-01' } }
$leLess than or equal to{ created_at: { $le: '2024-12-31' } }
$likePattern matching{ title: { $like: '%onboarding%' } }
$ilikeCase-insensitive pattern matching{ title: { $ilike: '%Customer%' } }
The $like and $ilike operators support SQL-style wildcards: % matches any characters
Comparison operators ($gt, $lt, $ge, $le) can be used for numbers and dates.

Filter Structure

The JSON filter object has two main sections:

Attributes

Filter on the resource’s direct attributes:
{
  "attributes": {
    "status": { "$eq": "pending" },
    "archived_at": { "$eq": null },
    "created_at": { "$gt": "2024-01-01T00:00:00Z" }
  }
}

Relationships

Filter on related resources:
{
  "relationships": {
    "owner": {
      "id": { "$eq": "abc-123" }
    },
    "company": {
      "attributes": {
        "slug": { "$eq": "acme-corp" }
      }
    }
  }
}

Examples by Resource

Filtering Cases

Direct Parameters:
# Filter by template, workspace, and status
GET /v1/cases?template[id]=template-123&workspace[slug]=acme&parent_status=in_progress
JSON Filter:
// Cases created after a date with pending status
const filter = {
  attributes: {
    created_at: { $gt: '2024-01-01T00:00:00Z' },
    parent_status: { $eq: 'pending' },
    archived_at: { $eq: null }
  },
  relationships: {
    workspace: {
      attributes: {
        slug: { $eq: 'acme-corp' }
      }
    }
  }
};

Filtering Case Templates

Direct Parameters:
# Search by title and filter by workspace
GET /v1/case_templates?title=onboarding&workspace[slug]=acme&archived=false
JSON Filter:
// Templates with specific title pattern
const filter = {
  attributes: {
    is_template: { $eq: true },
    title: { $ilike: '%customer%' },
    archived_at: { $eq: null }
  }
};

Filtering Forms

Direct Parameters:
# Filter by flow, user, and completion status
GET /v1/forms?flow[slug]=kyc&user[email]=[email protected]&completed=true
JSON Filter:
// Active forms with specific owner
const filter = {
  attributes: {
    active: { $eq: true },
    completed: { $eq: false }
  },
  relationships: {
    owner: {
      attributes: {
        email: { $eq: '[email protected]' }
      }
    }
  }
};

Common Filtering Patterns

Time-Based Filtering

// Cases created in the last 7 days
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();

const filter = {
  attributes: {
    created_at: { $gt: sevenDaysAgo }
  }
};

Status Filtering

// Active, non-archived cases in progress
const filter = {
  attributes: {
    parent_status: { $eq: 'in_progress' },
    archived_at: { $eq: null }
  }
};
// Case templates with "customer" in the title
const filter = {
  attributes: {
    title: { $ilike: '%customer%' }
  }
};

Multiple Criteria

// Complex filter combining multiple conditions
const filter = {
  attributes: {
    created_at: { $gt: '2024-01-01T00:00:00Z' },
    parent_status: { $ne: 'draft' },
    archived_at: { $eq: null }
  },
  relationships: {
    workspace: {
      attributes: {
        slug: { $eq: 'acme-corp' }
      }
    },
    owner: {
      attributes: {
        email: { $eq: '[email protected]' }
      }
    }
  }
};

Available Filters by Endpoint

Cases (/v1/cases)

Direct Parameters:
  • template[id] - Filter by template UUID
  • status - Custom status value
  • parent_status - Parent status (draft, pending, in_progress, closed)
  • archived - Boolean (true/false)
  • waiting_for - Who the case is waiting for (none, owner, contact)
  • reference - Custom reference number
  • owner[id] - Owner UUID
  • owner[email] - Owner email
  • workspace[id] - Workspace UUID
  • workspace[slug] - Workspace slug
  • contact[given_name] - Contact first name
  • contact[family_name] - Contact last name
  • contact[email] - Contact email

Case Templates (/v1/case_templates)

Direct Parameters:
  • title - Search by title (partial match)
  • reference - Custom reference
  • archived - Boolean (true/false)
  • owner[id] - Owner UUID
  • owner[email] - Owner email
  • workspace[id] - Workspace UUID
  • workspace[slug] - Workspace slug

Forms (/v1/forms)

Direct Parameters:
  • workspace[slug] - Workspace slug
  • flow[slug] - Form template slug (single value or array)
  • flow[customization] - Template customization UUID
  • owner[email] - Owner email
  • user[given_name] - User first name
  • user[family_name] - User last name
  • user[email] - User email
  • user[phone] - User phone
  • archived - Boolean (true/false)
  • active - Boolean (true/false)
  • completed - Boolean (true/false)
  • processed - Boolean (true/false)

Best Practices

For basic filtering needs (workspace, status, owner), use direct query parameters. They’re easier to read and debug.
When you need operators, date comparisons, or pattern matching, use the JSON filter method.
When passing JSON in the filter parameter, always use encodeURIComponent() to properly encode special characters.
Always use pagination when expecting many results to avoid timeouts and improve performance.
To check for null values, use { $eq: null } or { $ne: null } in your filter.
// Non-archived resources
{ archived_at: { $eq: null } }

Real-World Examples

Dashboard: Active Cases by Team Member

async function getActiveCases(ownerEmail) {
  const filter = {
    attributes: {
      parent_status: { $eq: 'in_progress' },
      archived_at: { $eq: null }
    },
    relationships: {
      owner: {
        attributes: {
          email: { $eq: ownerEmail }
        }
      }
    }
  };

  const response = await fetch(
    `https://connect.penbox.io/v1/cases?filter=${encodeURIComponent(JSON.stringify(filter))}&sort=-created_at`,
    { headers: { 'Authorization': `Bearer ${token}` } }
  );

  return await response.json();
}

Search Case Templates

async function searchTemplates(searchTerm) {
  // Using direct parameter for simplicity
  const params = new URLSearchParams({
    title: searchTerm,
    archived: 'false'
  });

  const response = await fetch(
    `https://connect.penbox.io/v1/case_templates?${params}&sort=title`,
    { headers: { 'Authorization': `Bearer ${token}` } }
  );

  return await response.json();
}

Multi-Workspace Reporting

async function getCompletedCases(workspaceSlug, startDate, endDate) {
  const filter = {
    attributes: {
      parent_status: { $eq: 'closed' },
      created_at: { $gt: startDate, $lt: endDate }
    },
    relationships: {
      workspace: {
        attributes: {
          slug: { $eq: workspaceSlug }
        }
      }
    }
  };

  const response = await fetch(
    `https://connect.penbox.io/v1/cases?filter=${encodeURIComponent(JSON.stringify(filter))}`,
    { headers: { 'Authorization': `Bearer ${token}` } }
  );

  return await response.json();
}

Error Handling

Invalid filters return a 400 Bad Request error:
{
  "errors": [
    {
      "status": "400",
      "title": "Invalid Filter",
      "detail": "Invalid filter parameter format"
    }
  ]
}
Common Issues:
  • Invalid JSON format in filter parameter
  • Unrecognized filter fields
  • Type mismatches (e.g., string instead of boolean)
  • Invalid operator usage
Solution:
try {
  const response = await fetch(url, { headers });
  
  if (!response.ok) {
    const error = await response.json();
    console.error('Filter error:', error);
    return { data: [], meta: { total_count: 0 } };
  }
  
  return await response.json();
} catch (error) {
  console.error('Request failed:', error);
  return { data: [], meta: { total_count: 0 } };
}

Next Steps