From 4e82458c66fe7455e5bc5ffbff163ffe1bea4c44 Mon Sep 17 00:00:00 2001
From: Robert McLeod <robert.mcleod@readify.net>
Date: Wed, 11 Dec 2019 15:18:18 +1100
Subject: [PATCH] initial import

---
 .gitignore                                  |   2 +
 PwshZendesk.psd1                            | 193 +++++++++++++++
 PwshZendesk.psm1                            |  10 +
 functions/Add-Tag.ps1                       |  61 +++++
 functions/Connect-.ps1                      |  36 +++
 functions/Export-Organization.ps1           |  25 ++
 functions/Export-Sample.ps1                 |  31 +++
 functions/Export-Ticket.ps1                 |  26 ++
 functions/Export-TicketEvent.ps1            |  41 ++++
 functions/Export-User.ps1                   |  26 ++
 functions/Get-Attachment.ps1                |  26 ++
 functions/Get-AuthenticatedUser.ps1         |  18 ++
 functions/Get-Comment.ps1                   |  25 ++
 functions/Get-Connection.ps1                |  35 +++
 functions/Get-DeletedTicket.ps1             |  19 ++
 functions/Get-DeletedUser.ps1               |  32 +++
 functions/Get-Group.ps1                     |  78 ++++++
 functions/Get-GroupMembership.ps1           |  96 ++++++++
 functions/Get-Incident.ps1                  |  26 ++
 functions/Get-JobStatus.ps1                 |  40 ++++
 functions/Get-OrganizationMembership.ps1    |  61 +++++
 functions/Get-Problem.ps1                   |  20 ++
 functions/Get-SearchCount.ps1               |  27 +++
 functions/Get-SuspendedTicket.ps1           |  32 +++
 functions/Get-Tag.ps1                       |  57 +++++
 functions/Get-Ticket.ps1                    | 147 ++++++++++++
 functions/Get-TicketCollaborator.ps1        |  26 ++
 functions/Get-TicketEmailCC.ps1             |  26 ++
 functions/Get-TicketFollower.ps1            |  26 ++
 functions/Get-TicketRelated.ps1             |  26 ++
 functions/Get-User.ps1                      | 142 +++++++++++
 functions/Get-UserRelated.ps1               |  24 ++
 functions/Hide-Comment.ps1                  |  31 +++
 functions/Import-Ticket.ps1                 |  36 +++
 functions/Invoke-Method.ps1                 | 195 +++++++++++++++
 functions/Merge-User.ps1                    |  39 +++
 functions/New-Group.ps1                     |  33 +++
 functions/New-GroupMembership.ps1           |  46 ++++
 functions/New-OrganizationMembership.ps1    |  46 ++++
 functions/Remove-Attachment.ps1             |  28 +++
 functions/Remove-Group.ps1                  |  28 +++
 functions/Remove-GroupMembership.ps1        |  34 +++
 functions/Remove-OrganizationMembership.ps1 |  34 +++
 functions/Remove-SuspendedTicket.ps1        |  33 +++
 functions/Remove-Tag.ps1                    |  64 +++++
 functions/Remove-Ticket.ps1                 |  47 ++++
 functions/Remove-User.ps1                   |  37 +++
 functions/Restore-DeletedTicket.ps1         |  32 +++
 functions/Restore-SuspendedTicket.ps1       |  33 +++
 functions/Search-.ps1                       |  34 +++
 functions/Set-Tag.ps1                       |  64 +++++
 functions/Set-User.ps1                      | 248 ++++++++++++++++++++
 functions/Test-Connection.ps1               |  26 ++
 functions/Update-Group.ps1                  |  40 ++++
 tests/Invoke-Method.tests.ps1               | 226 ++++++++++++++++++
 todo.md                                     |  11 +
 56 files changed, 2905 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 PwshZendesk.psd1
 create mode 100644 PwshZendesk.psm1
 create mode 100644 functions/Add-Tag.ps1
 create mode 100644 functions/Connect-.ps1
 create mode 100644 functions/Export-Organization.ps1
 create mode 100644 functions/Export-Sample.ps1
 create mode 100644 functions/Export-Ticket.ps1
 create mode 100644 functions/Export-TicketEvent.ps1
 create mode 100644 functions/Export-User.ps1
 create mode 100644 functions/Get-Attachment.ps1
 create mode 100644 functions/Get-AuthenticatedUser.ps1
 create mode 100644 functions/Get-Comment.ps1
 create mode 100644 functions/Get-Connection.ps1
 create mode 100644 functions/Get-DeletedTicket.ps1
 create mode 100644 functions/Get-DeletedUser.ps1
 create mode 100644 functions/Get-Group.ps1
 create mode 100644 functions/Get-GroupMembership.ps1
 create mode 100644 functions/Get-Incident.ps1
 create mode 100644 functions/Get-JobStatus.ps1
 create mode 100644 functions/Get-OrganizationMembership.ps1
 create mode 100644 functions/Get-Problem.ps1
 create mode 100644 functions/Get-SearchCount.ps1
 create mode 100644 functions/Get-SuspendedTicket.ps1
 create mode 100644 functions/Get-Tag.ps1
 create mode 100644 functions/Get-Ticket.ps1
 create mode 100644 functions/Get-TicketCollaborator.ps1
 create mode 100644 functions/Get-TicketEmailCC.ps1
 create mode 100644 functions/Get-TicketFollower.ps1
 create mode 100644 functions/Get-TicketRelated.ps1
 create mode 100644 functions/Get-User.ps1
 create mode 100644 functions/Get-UserRelated.ps1
 create mode 100644 functions/Hide-Comment.ps1
 create mode 100644 functions/Import-Ticket.ps1
 create mode 100644 functions/Invoke-Method.ps1
 create mode 100644 functions/Merge-User.ps1
 create mode 100644 functions/New-Group.ps1
 create mode 100644 functions/New-GroupMembership.ps1
 create mode 100644 functions/New-OrganizationMembership.ps1
 create mode 100644 functions/Remove-Attachment.ps1
 create mode 100644 functions/Remove-Group.ps1
 create mode 100644 functions/Remove-GroupMembership.ps1
 create mode 100644 functions/Remove-OrganizationMembership.ps1
 create mode 100644 functions/Remove-SuspendedTicket.ps1
 create mode 100644 functions/Remove-Tag.ps1
 create mode 100644 functions/Remove-Ticket.ps1
 create mode 100644 functions/Remove-User.ps1
 create mode 100644 functions/Restore-DeletedTicket.ps1
 create mode 100644 functions/Restore-SuspendedTicket.ps1
 create mode 100644 functions/Search-.ps1
 create mode 100644 functions/Set-Tag.ps1
 create mode 100644 functions/Set-User.ps1
 create mode 100644 functions/Test-Connection.ps1
 create mode 100644 functions/Update-Group.ps1
 create mode 100644 tests/Invoke-Method.tests.ps1
 create mode 100644 todo.md

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9bea433
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+
+.DS_Store
diff --git a/PwshZendesk.psd1 b/PwshZendesk.psd1
new file mode 100644
index 0000000..8530ece
--- /dev/null
+++ b/PwshZendesk.psd1
@@ -0,0 +1,193 @@
+#
+# Module manifest for module 'PwshZendesk'
+#
+# Generated by: Robert McLeod
+#
+# Generated on: 15/9/19
+#
+
+@{
+
+    # Script module or binary module file associated with this manifest.
+    RootModule = '.\PwshZendesk.psm1'
+
+    # Version number of this module.
+    ModuleVersion = '999.999'
+
+    # Supported PSEditions
+    CompatiblePSEditions = @(
+        'Core'
+        'Desktop'
+    )
+
+    # ID used to uniquely identify this module
+    GUID = 'fd14fe05-21fb-4dfb-969b-7012632b461e'
+
+    # Author of this module
+    Author = 'Telstra Purple DevSecOps'
+
+    # Company or vendor of this module
+    CompanyName = 'Telstra Purple Pty Ltd'
+
+    # Copyright statement for this module
+    Copyright = '(c) 2019 Telstra Purple Pty Ltd. All rights reserved.'
+
+    # Description of the functionality provided by this module
+    Description = 'Wrapper for the Zendesk Rest API'
+
+    # Minimum version of the PowerShell engine required by this module
+    PowerShellVersion = '5.0'
+
+    # Name of the PowerShell host required by this module
+    # PowerShellHostName = ''
+
+    # Minimum version of the PowerShell host required by this module
+    # PowerShellHostVersion = ''
+
+    # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
+    # DotNetFrameworkVersion = ''
+
+    # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
+    # CLRVersion = ''
+
+    # Processor architecture (None, X86, Amd64) required by this module
+    # ProcessorArchitecture = ''
+
+    # Modules that must be imported into the global environment prior to importing this module
+    # RequiredModules = @()
+
+    # Assemblies that must be loaded prior to importing this module
+    # RequiredAssemblies = @()
+
+    # Script files (.ps1) that are run in the caller's environment prior to importing this module.
+    # ScriptsToProcess = @()
+
+    # Type files (.ps1xml) to be loaded when importing this module
+    # TypesToProcess = @()
+
+    # Format files (.ps1xml) to be loaded when importing this module
+    # FormatsToProcess = @()
+
+    # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
+    # NestedModules = @()
+
+    # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
+    FunctionsToExport = @(
+        'Add-Tag',                             'Add-ZendeskTag'
+        'Connect-',                            'Connect-Zendesk'
+        'Export-Organization',                 'Export-ZendeskOrganization'
+        'Export-Sample',                       'Export-ZendeskSample'
+        'Export-Ticket',                       'Export-ZendeskTicket'
+        'Export-TicketEvent',                  'Export-ZendeskTicketEvent'
+        'Export-User',                         'Export-ZendeskUser'
+        'Get-Attachment',                      'Get-ZendeskAttachment'
+        'Get-AuthenticatedUser',               'Get-ZendeskAuthenticatedUser'
+        'Get-Comment',                         'Get-ZendeskComment'
+        'Get-Connection',                      'Get-ZendeskConnection'
+        'Get-DeletedTicket',                   'Get-ZendeskDeletedTicket'
+        'Get-DeletedUser',                     'Get-ZendeskDeletedUser'
+        'Get-Group',                           'Get-ZendeskGroup'
+        'Get-GroupMembership',                 'Get-ZendeskGroupMembership'
+        'Get-Incident',                        'Get-ZendeskIncident'
+        'Get-JobStatus',                       'Get-ZendeskJobStatus'
+        'Get-OrganizationMembership',          'Get-ZendeskOrganizationMembership'
+        'Get-Problem',                         'Get-ZendeskProblem'
+        'Get-SearchCount',                     'Get-ZendeskSearchCount'
+        'Get-SuspendedTicket',                 'Get-ZendeskSuspendedTicket'
+        'Get-Tag',                             'Get-ZendeskTag'
+        'Get-Ticket',                          'Get-ZendeskTicket'
+        'Get-TicketCollaborator',              'Get-ZendeskTicketCollaborator'
+        'Get-TicketEmailCC',                   'Get-ZendeskTicketEmailCC'
+        'Get-TicketFollower',                  'Get-ZendeskTicketFollower'
+        'Get-TicketRelated',                   'Get-ZendeskTicketRelated'
+        'Get-User',                            'Get-ZendeskUser'
+        'Get-UserRelated',                     'Get-ZendeskUserRelated'
+        'Hide-Comment',                        'Hide-ZendeskComment'
+        'Import-Ticket',                       'Import-ZendeskTicket'
+        'Invoke-Method',                       'Invoke-ZendeskMethod'
+        'Merge-Ticket',                        'Merge-ZendeskTicket'
+        'Merge-User',                          'Merge-ZendeskUser'
+        'New-Attachment',                      'New-ZendeskAttachment'
+        'New-Group',                           'New-ZendeskGroup'
+        'New-GroupMembership',                 'New-ZendeskGroupMembership'
+        'New-OrganizationMembership',          'New-ZendeskOrganizationMembership'
+        'New-Ticket',                          'New-ZendeskTicket'
+        'Remove-Attachment',                   'Remove-ZendeskAttachment'
+        'Remove-Group',                        'Remove-ZendeskGroup'
+        'Remove-GroupMembership',              'Remove-ZendeskGroupMembership'
+        'Remove-OrganizationMembership',       'Remove-ZendeskOrganizationMembership'
+        'Remove-SuspendedTicket',              'Remove-ZendeskSuspendedTicket'
+        'Remove-Tag',                          'Remove-ZendeskTag'
+        'Remove-Ticket',                       'Remove-ZendeskTicket'
+        'Remove-User',                         'Remove-ZendeskUser'
+        'Restore-DeletedTicket',               'Restore-ZendeskDeletedTicket'
+        'Restore-SuspendedTicket',             'Restore-ZendeskSuspendedTicket'
+        'Search-',                             'Search-Zendesk'
+        'Set-GroupMembershipAsDefault',        'Set-ZendeskGroupMembershipAsDefault'
+        'Set-OrganizationMembershipAsDefault', 'Set-ZendeskOrganizationMembershipAsDefault'
+        'Set-Tag',                             'Set-ZendeskTag'
+        'Set-User',                            'Set-ZendeskUser'
+        'Test-Connection',                     'Test-ZendeskConnection'
+        'Update-Group',                        'Update-ZendeskGroup'
+        'Update-Ticket',                       'Update-ZendeskTicket'
+    )
+
+    # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
+    CmdletsToExport = @()
+
+    # Variables to export from this module
+    # VariablesToExport = @()
+
+    # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export.
+    AliasesToExport = @()
+
+    # DSC resources to export from this module
+    # DscResourcesToExport = @()
+
+    # List of all modules packaged with this module
+    # ModuleList = @()
+
+    # List of all files packaged with this module
+    # FileList = @()
+
+    # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.
+    PrivateData = @{
+
+        PSData = @{
+
+            # Tags applied to this module. These help with module discovery in online galleries.
+            # Tags = @()
+
+            # A URL to the license for this module.
+            # LicenseUri = ''
+
+            # A URL to the main website for this project.
+            ProjectUri = 'https://github.com/Readify/PwshZendesk'
+
+            # A URL to an icon representing this module.
+            # IconUri = ''
+
+            # ReleaseNotes of this module
+            # ReleaseNotes = ''
+
+            # Prerelease string of this module
+            # Prerelease = ''
+
+            # Flag to indicate whether the module requires explicit user acceptance for install/update/save
+            # RequireLicenseAcceptance = $false
+
+            # External dependent modules of this module
+            # ExternalModuleDependencies = @()
+
+        } # End of PSData hashtable
+
+    } # End of PrivateData hashtable
+
+    # HelpInfo URI of this module
+    # HelpInfoURI = ''
+
+    # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix.
+    DefaultCommandPrefix = 'Zendesk'
+
+}
+
diff --git a/PwshZendesk.psm1 b/PwshZendesk.psm1
new file mode 100644
index 0000000..2165532
--- /dev/null
+++ b/PwshZendesk.psm1
@@ -0,0 +1,10 @@
+
+Set-StrictMode -Version Latest
+
+Get-ChildItem -Path "$PSScriptRoot\functions" -Filter '*.ps1' -Recurse | ForEach-Object {
+    . $_.FullName
+    Export-ModuleMember -Function $_.BaseName
+}
+
+$Script:NotConnectedMessage = 'No connection supplied or stored. Please either call `Connect-Zendesk` or call `Get-ZendeskConnection` and pass the result to all additional calls.'
+$Script:InvalidConnection = 'Provided connection is invalid.'
diff --git a/functions/Add-Tag.ps1 b/functions/Add-Tag.ps1
new file mode 100644
index 0000000..3711fdb
--- /dev/null
+++ b/functions/Add-Tag.ps1
@@ -0,0 +1,61 @@
+
+function Add-Tag {
+    [OutputType([PSCustomObject])]
+    [CmdletBinding(DefaultParameterSetName = 'Default')]
+    Param (
+
+        # Unique Id of ticket to add tags to
+        [Parameter(Mandatory = $true,
+            ParameterSetName = 'Ticket')]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $TicketId,
+
+        # Unique Id of organization to add tags to
+        [Parameter(Mandatory = $true,
+            ParameterSetName = 'Org')]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $OrganizationId,
+
+        # Unique Id of user to add tags to
+        [Parameter(Mandatory = $true,
+            ParameterSetName = 'User')]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $UserId,
+
+        # Tags to add to entity
+        [Parameter(Mandatory = $true)]
+        [String[]]
+        $Tag,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    switch ($PSCMDlet.ParameterSetName) {
+        'Ticket' {
+            $path = "/api/v2/tickets/$TicketId/tags.json"
+        }
+
+        'Org' {
+            $path = "/api/v2/organizations/$OrganizationId/tags.json"
+        }
+
+        'User' {
+            $path = "/api/v2/users/$UserId/tags.json"
+        }
+    }
+
+    $body = {
+        tags = $Tag
+    }
+
+    $result = Invoke-Method -Context $Context -Method 'Put' -Path $path -Body $body -Verbose:$VerbosePreference
+    $result
+
+}
diff --git a/functions/Connect-.ps1 b/functions/Connect-.ps1
new file mode 100644
index 0000000..b2549b2
--- /dev/null
+++ b/functions/Connect-.ps1
@@ -0,0 +1,36 @@
+
+function Connect- {
+    [OutputType([String])]
+    [CmdletBinding()]
+    Param (
+
+        # Zendesk subdomain
+        [Parameter(Mandatory = $true)]
+        [ValidateNotNullOrEmpty()]
+        [String]
+        $Organization,
+
+        # Email address of user to log in
+        [Parameter(Mandatory = $true)]
+        [ValidateNotNullOrEmpty()]
+        [String]
+        $Username,
+
+        # Zendesk API key retrieved from https://<organization>.zendesk.com/agent/admin/api/settings
+        [Parameter(Mandatory = $true)]
+        [ValidateNotNullOrEmpty()]
+        [SecureString]
+        $ApiKey
+    )
+
+    $context = Get-Connection @PSBoundParameters
+
+    if (Test-Connection -context $context) {
+        $Script:Context = $context
+    } else {
+        throw $Script:InvalidConnection
+    }
+
+    $true
+
+}
diff --git a/functions/Export-Organization.ps1 b/functions/Export-Organization.ps1
new file mode 100644
index 0000000..cefc620
--- /dev/null
+++ b/functions/Export-Organization.ps1
@@ -0,0 +1,25 @@
+
+function Export-Organization {
+    [OutputType([PSCustomObject])]
+    [CMDletBinding()]
+    Param (
+
+        # Timestamp returned by the last Export or `0` for a new incremental export
+        [Parameter(Mandatory = $true)]
+        [ValidateRange(0, [Int64]::MaxValue)]
+        [Int64]
+        $Timestamp,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    $path = "/api/v2/incremental/organizations.json?start_time=$Timestamp"
+
+    $result = Invoke-Method -Context $Context -Path $path -Pagination $false -Verbose:$VerbosePreference
+    $result
+
+}
diff --git a/functions/Export-Sample.ps1 b/functions/Export-Sample.ps1
new file mode 100644
index 0000000..97a04df
--- /dev/null
+++ b/functions/Export-Sample.ps1
@@ -0,0 +1,31 @@
+
+function Export-Sample {
+    [OutputType([PSCustomObject])]
+    [CMDletBinding()]
+    Param (
+
+        # Name of the entity to get a sample export for
+        [Parameter(Mandatory = $true)]
+        [ValidateSet('tickets', 'users', 'organizations')]
+        [String]
+        $EntityName,
+
+        # Timestamp returned by the last Export or `0` for a new incremental export
+        [Parameter(Mandatory = $true)]
+        [ValidateRange(0, [Int64]::MaxValue)]
+        [Int64]
+        $Timestamp,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    $path = "/api/v2/incremental/$EntityName/sample.json?start_time=$Timestamp"
+
+    $result = Invoke-Method -Context $Context -Path $path -Pagination $false -Verbose:$VerbosePreference
+    $result
+
+}
diff --git a/functions/Export-Ticket.ps1 b/functions/Export-Ticket.ps1
new file mode 100644
index 0000000..ed78e54
--- /dev/null
+++ b/functions/Export-Ticket.ps1
@@ -0,0 +1,26 @@
+
+function Export-Ticket {
+
+    [OutputType([PSCustomObject])]
+    [CMDletBinding()]
+    Param (
+
+        # Timestamp returned by the last Export or `0` for a new incremental export
+        [Parameter(Mandatory = $true)]
+        [ValidateRange(0, [Int64]::MaxValue)]
+        [Int64]
+        $Timestamp,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    $path = "/api/v2/incremental/tickets.json?start_time=$Timestamp"
+
+    $result = Invoke-Method -Context $Context -Path $path -Pagination $false -Verbose:$VerbosePreference
+    $result
+
+}
diff --git a/functions/Export-TicketEvent.ps1 b/functions/Export-TicketEvent.ps1
new file mode 100644
index 0000000..b22a589
--- /dev/null
+++ b/functions/Export-TicketEvent.ps1
@@ -0,0 +1,41 @@
+
+function Export-TicketEvent {
+
+    [OutputType([PSCustomObject])]
+    [CMDletBinding()]
+    Param (
+
+        # Timestamp returned by the last Export or `0` for a new incremental export
+        [Parameter(Mandatory = $true)]
+        [ValidateRange(0, [Int64]::MaxValue)]
+        [Int64]
+        $Timestamp,
+
+        # Entities to sideload in the request
+        [Parameter(Mandatory = $false)]
+        [ValidateSet('comment_events')]
+        [String[]]
+        $SideLoad,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    $params = @{
+        Context    = $Context
+        Path       = "/api/v2/incremental/ticket_events.json?start_time=$Timestamp"
+        Pagination = $false
+        Verbose    = $VerbosePreference
+    }
+
+    if ($PSBoundParameters.ContainsKey('SideLoad')) {
+        $params.SideLoad = $SideLoad
+    }
+
+    $result = Invoke-Method @params
+    $result
+
+}
diff --git a/functions/Export-User.ps1 b/functions/Export-User.ps1
new file mode 100644
index 0000000..d166949
--- /dev/null
+++ b/functions/Export-User.ps1
@@ -0,0 +1,26 @@
+
+function Export-User {
+
+    [OutputType([PSCustomObject])]
+    [CMDletBinding()]
+    Param (
+
+        # Timestamp returned by the last Export or `0` for a new incremental export
+        [Parameter(Mandatory = $true)]
+        [ValidateRange(0, [Int64]::MaxValue)]
+        [Int64]
+        $Timestamp,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    $path = "/api/v2/incremental/users.json?start_time=$Timestamp"
+
+    $result = Invoke-Method -Context $Context -Path $path -Pagination $false -Verbose:$VerbosePreference
+    $result
+
+}
diff --git a/functions/Get-Attachment.ps1 b/functions/Get-Attachment.ps1
new file mode 100644
index 0000000..1f406f4
--- /dev/null
+++ b/functions/Get-Attachment.ps1
@@ -0,0 +1,26 @@
+
+function Get-Attachment {
+
+    [OutputType([PSCustomObject])]
+    [CmdletBinding()]
+    Param (
+
+        # Unique Id of the attachment to retrieve
+        [Parameter(Mandatory = $true)]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $Id,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    $path = "/api/v2/attachments/$Id.json"
+
+    $result = Invoke-Method -Context $Context -Path $path -Verbose:$VerbosePreference
+    $result
+
+}
diff --git a/functions/Get-AuthenticatedUser.ps1 b/functions/Get-AuthenticatedUser.ps1
new file mode 100644
index 0000000..a4203ff
--- /dev/null
+++ b/functions/Get-AuthenticatedUser.ps1
@@ -0,0 +1,18 @@
+
+function Get-AuthenticatedUser {
+
+    [OutputType([PSCustomObject])]
+    [CMDletBinding()]
+    Param (
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    $result = Invoke-Method -Context $Context -Path "/api/v2/users/me.json" -Verbose:$VerbosePreference
+    $result | Select-Object -Expand 'user'
+
+}
diff --git a/functions/Get-Comment.ps1 b/functions/Get-Comment.ps1
new file mode 100644
index 0000000..853c32b
--- /dev/null
+++ b/functions/Get-Comment.ps1
@@ -0,0 +1,25 @@
+
+function Get-Comment {
+
+    [OutputType([String])]
+    [CmdletBinding()]
+    Param (
+
+        # Unique Id of the ticket to get comments for
+        [Parameter(Mandatory = $true)]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $TicketId,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    $path = "/api/v2/tickets/$TicketId/comments.json"
+
+    $result = Invoke-Method -Context $Context -Path $path -Verbose:$VerbosePreference
+    $result | Select-Object -Expand 'comments'
+}
diff --git a/functions/Get-Connection.ps1 b/functions/Get-Connection.ps1
new file mode 100644
index 0000000..e3cf2a0
--- /dev/null
+++ b/functions/Get-Connection.ps1
@@ -0,0 +1,35 @@
+
+function Get-Connection {
+
+    [OutputType([PSCustomObject])]
+    [CMDletBinding()]
+    Param (
+        # Zendesk subdomain
+        [Parameter(Mandatory = $true)]
+        [ValidateNotNullOrEmpty()]
+        [String]
+        $Organization,
+
+        # Email address of user to log in
+        [Parameter(Mandatory = $true)]
+        [ValidateNotNullOrEmpty()]
+        [String]
+        $Username,
+
+        # Zendesk API key retrieved from https://<organization>.zendesk.com/agent/admin/api/settings
+        [Parameter(Mandatory = $true)]
+        [ValidateNotNullOrEmpty()]
+        [SecureString]
+        $ApiKey
+    )
+
+    $context = [PSCustomObject]@{
+        Organization = $Organization
+        BaseUrl      = "https://$Organization.zendesk.com"
+        Credential   = [System.Management.Automation.PSCredential]::New("$Username/token", $ApiKey)
+    }
+
+    $context | Add-Member -TypeName 'ZendeskContext'
+    $context
+
+}
diff --git a/functions/Get-DeletedTicket.ps1 b/functions/Get-DeletedTicket.ps1
new file mode 100644
index 0000000..d37783e
--- /dev/null
+++ b/functions/Get-DeletedTicket.ps1
@@ -0,0 +1,19 @@
+
+function Get-DeletedTicket {
+
+    [OutputType([PSCustomObject])]
+    [CMDletBinding()]
+    Param (
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    $path = '/api/v2/deleted_tickets.json'
+
+    $result = Invoke-Method -Context $Context -Path $path -Verbose:$VerbosePreference
+    $result | Select-Object -Expand 'deleted_tickets'
+
+}
diff --git a/functions/Get-DeletedUser.ps1 b/functions/Get-DeletedUser.ps1
new file mode 100644
index 0000000..1249e54
--- /dev/null
+++ b/functions/Get-DeletedUser.ps1
@@ -0,0 +1,32 @@
+
+function Get-DeletedUser {
+
+    [OutputType([PSCustomObject])]
+    [CMDletBinding()]
+    Param (
+
+        # Unique Id of the User to retrieve
+        [Parameter(Mandatory = $false)]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $UserId,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    if ($PSBoundParameters.ContainsKey('UserId')) {
+        $path = "/api/v2/deleted_users/$UserId.json"
+        $key = 'deleted_user'
+    } else {
+        $path = '/api/v2/deleted_users.json'
+        $key = 'deleted_users'
+    }
+
+    $result = Invoke-Method -Context $Context -Path $path -Verbose:$VerbosePreference
+    $result | Select-Object -Expand $key
+
+}
diff --git a/functions/Get-Group.ps1 b/functions/Get-Group.ps1
new file mode 100644
index 0000000..1bbf50a
--- /dev/null
+++ b/functions/Get-Group.ps1
@@ -0,0 +1,78 @@
+
+function Get-Group {
+
+    [OutputType([PSCustomObject])]
+    [CmdletBinding(DefaultParameterSetName = 'Default')]
+    Param (
+
+        # Unique Id of the group to retrieve
+        [Parameter(Mandatory = $true,
+            ParameterSetName = 'Id')]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $Id,
+
+        # Unique Id of the User whose groups to retrieve
+        [Parameter(Mandatory = $true,
+            ParameterSetName = 'UserId')]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $UserId,
+
+        # Only retrieve groups that can be assigned
+        [Parameter(Mandatory = $true,
+            ParameterSetName = 'Assignable')]
+        [Switch]
+        $Assignable,
+
+        #
+        [Parameter(Mandatory = $false)]
+        [ValidateNotNullOrEmpty()]
+        [ValidateSet(
+            'users'
+        )]
+        [String[]]
+        $SideLoad,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    $key = 'groups'
+
+    switch ($PSCMDlet.ParameterSetName) {
+        'Id' {
+            $path = "/api/v2/groups/$Id.json"
+            $key = 'group'
+        }
+
+        'UserId' {
+            $path = "/api/v2/users/$UserId/groups.json"
+        }
+
+        'Assignable' {
+            $path = '/api/v2/groups/assignable.json'
+        }
+
+        default {
+            $path = '/api/v2/groups.json'
+        }
+    }
+
+    $params = @{
+        Context = $Context
+        Path    = $path
+        Verbose = $VerbosePreference
+    }
+
+    if ($PSBoundParameters.ContainsKey('SideLoad')) {
+        $params.SideLoad = $SideLoad
+    }
+
+    $result = Invoke-Method @params
+    $result | Select-Object -Expand $key
+
+}
diff --git a/functions/Get-GroupMembership.ps1 b/functions/Get-GroupMembership.ps1
new file mode 100644
index 0000000..84f4660
--- /dev/null
+++ b/functions/Get-GroupMembership.ps1
@@ -0,0 +1,96 @@
+
+function Get-GroupMembership {
+
+    [OutputType([PSCustomObject])]
+    [CmdletBinding(DefaultParameterSetName = 'Default')]
+    Param (
+
+        # Unique Id of the group membership to retrieve
+        [Parameter(Mandatory = $true,
+            ParameterSetName = 'Id')]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $Id,
+
+        # Unique Id of the user to get group memberships for
+        [Parameter(Mandatory = $true,
+            ParameterSetName = 'UserId')]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $UserId,
+
+        # Unique Id of the group to get membersips for
+        [Parameter(Mandatory = $true,
+            ParameterSetName = 'GroupId')]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $GroupId,
+
+        # Only retrieve assignable memberships
+        [Parameter(Mandatory = $false,
+            ParameterSetName = 'GroupId')]
+        [Parameter(Mandatory = $false,
+            ParameterSetName = 'Default')]
+        [Switch]
+        $Assignable,
+
+        # Entities to sideload in the request
+        [Parameter(Mandatory = $false)]
+        [ValidateNotNullOrEmpty()]
+        [ValidateSet(
+            'groups',
+            'users'
+        )]
+        [String[]]
+        $SideLoad,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    $key = 'group_memberships'
+
+    switch ($PSCMDlet.ParameterSetName) {
+        'Id' {
+            $path = "/api/v2/group_memberships/$Id.json"
+            $key = 'group_membership'
+        }
+
+        'UserId' {
+            $path = "/api/v2/users/$UserId/group_memberships.json"
+        }
+
+        'GroupId' {
+            if ($Assignable) {
+                $path = "/api/v2/groups/$GroupId/memberships/assignable.json"
+            } else {
+                $path = "/api/v2/groups/$GroupId/memberships.json"
+            }
+        }
+
+        default {
+            if ($Assignable) {
+                $path = '/api/v2/group_memberships/assignable.json'
+            } else {
+                $path = '/api/v2/group_memberships.json'
+            }
+        }
+    }
+
+    $params = @{
+        Context = $Context
+        Path    = $path
+        Verbose = $VerbosePreference
+    }
+
+    if ($PSBoundParameters.ContainsKey('SideLoad')) {
+        $params.SideLoad = $SideLoad
+    }
+
+    $result = Invoke-Method @params
+    $result | Select-Object -Expand $key
+
+}
diff --git a/functions/Get-Incident.ps1 b/functions/Get-Incident.ps1
new file mode 100644
index 0000000..fd22fd3
--- /dev/null
+++ b/functions/Get-Incident.ps1
@@ -0,0 +1,26 @@
+
+function Get-Incident {
+
+    [OutputType([PSCustomObject])]
+    [CMDletBinding()]
+    Param (
+
+        # Unique Id of the problem ticket to get linked incidents for
+        [Parameter(Mandatory = $true)]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $TicketId,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    $path = "/api/v2/tickets/$TicketId/incidents.json"
+
+    $result = Invoke-Method -Context $Context -Path $path -Verbose:$VerbosePreference
+    $result | Select-Object -Expand 'tickets'
+
+}
diff --git a/functions/Get-JobStatus.ps1 b/functions/Get-JobStatus.ps1
new file mode 100644
index 0000000..4f0c630
--- /dev/null
+++ b/functions/Get-JobStatus.ps1
@@ -0,0 +1,40 @@
+
+function Get-JobStatus {
+
+    [OutputType([PSCustomObject])]
+    [CmdletBinding()]
+    Param (
+
+        # Unique Id of the background job whose status to retrieve
+        [Parameter(Mandatory = $false)]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64[]]
+        $Id,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    $key = 'job_statuses'
+
+    if ($PSBoundParameters.ContainsKey('Id')) {
+
+        if ($Id.count -gt 1) {
+            $ids = $Id -join ','
+            $path = "/api/v2/job_statuses/show_many.json?ids=$ids"
+        } else {
+            $path = "/api/v2/job_statuses/$Id.json"
+            $key = 'job_status'
+        }
+
+    } else {
+        $path = '/api/v2/job_statuses.json'
+    }
+
+    $result = Invoke-Method -Context $Context -Path $path -Verbose:$VerbosePreference
+    $result | Select-Object -Expand $key
+
+}
diff --git a/functions/Get-OrganizationMembership.ps1 b/functions/Get-OrganizationMembership.ps1
new file mode 100644
index 0000000..624b3d1
--- /dev/null
+++ b/functions/Get-OrganizationMembership.ps1
@@ -0,0 +1,61 @@
+
+function Get-OrganizationMembership {
+
+    [OutputType([PSCustomObject])]
+    [CmdletBinding(DefaultParameterSetName = 'Default')]
+    Param (
+
+        # Unique Id of the organization membership to retrieve
+        [Parameter(Mandatory = $true,
+            ParameterSetName = 'Id')]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $Id,
+
+        # Unique Id of the user to get organization memberships for
+        [Parameter(Mandatory = $true,
+            ParameterSetName = 'UserId')]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $UserId,
+
+        # Unique Id of the organization to get memberships for
+        [Parameter(Mandatory = $true,
+            ParameterSetName = 'OrganizationId')]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $OrganizationId,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    $key = 'organization_memberships'
+
+    switch ($PSCMDlet.ParameterSetName) {
+        'Id' {
+            $path = "/api/v2/organization_memberships/$Id.json"
+            $key = 'organization_membership'
+        }
+
+        'UserId' {
+            $path = "api/v2/users/$UserId/organization_memberships.json"
+        }
+
+        'OrganizationId' {
+            $path = "/api/v2/organizations/$OrganizationId/organization_memberships.json"
+        }
+
+        default {
+            $path = '/api/v2/organization_memberships.json'
+        }
+    }
+
+    $result = Invoke-Method -Context $Context -Path $path -Verbose:$VerbosePreference
+    $result | Select-Object -Expand $key
+
+}
+
diff --git a/functions/Get-Problem.ps1 b/functions/Get-Problem.ps1
new file mode 100644
index 0000000..25c6b77
--- /dev/null
+++ b/functions/Get-Problem.ps1
@@ -0,0 +1,20 @@
+
+function Get-Problem {
+
+    [OutputType([PSCustomObject])]
+    [CMDletBinding()]
+    Param (
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    $path = '/api/v2/problems.json'
+
+    $result = Invoke-Method -Context $Context -Path $path -Verbose:$VerbosePreference
+    $result | Select-Object -Expand 'tickets'
+
+}
diff --git a/functions/Get-SearchCount.ps1 b/functions/Get-SearchCount.ps1
new file mode 100644
index 0000000..7015e99
--- /dev/null
+++ b/functions/Get-SearchCount.ps1
@@ -0,0 +1,27 @@
+
+function Get-SearchCount {
+
+    [OutputType([String])]
+    [CmdletBinding()]
+    Param (
+        # Zendesk Query
+        [Parameter(Mandatory = $true)]
+        [ValidateNotNullOrEmpty()]
+        [String]
+        $Query,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    Write-Debug -Message "Query: $Query"
+    $Query = [uri]::EscapeDataString($Query)
+    Write-Debug -Message "Escaped Query: $Query"
+
+    $results = Invoke-Method -Context $Context -Path "/api/v2/search/count.json?query=$Query" -Pagination $false
+    $results | Select-Object -Expand 'count'
+
+}
diff --git a/functions/Get-SuspendedTicket.ps1 b/functions/Get-SuspendedTicket.ps1
new file mode 100644
index 0000000..3c246b9
--- /dev/null
+++ b/functions/Get-SuspendedTicket.ps1
@@ -0,0 +1,32 @@
+
+function Get-SuspendedTicket {
+
+    [OutputType([PSCustomObject])]
+    [CMDletBinding()]
+    Param (
+
+        # Unique Id of the suspended ticket to retrieve
+        [Parameter(Mandatory = $false)]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $Id,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    if ($PSBoundParameter.containsKey('UserId')) {
+        $path = "/api/v2/suspended_tickets/{id}.json"
+        $key = 'ticket'
+    } else {
+        $path = '/api/v2/suspended_tickets.json'
+        $key = 'tickets'
+    }
+
+    $result = Invoke-Method -Context $Context -Path $path -Verbose:$VerbosePreference
+    $result | Select-Object -Expand $key
+
+}
diff --git a/functions/Get-Tag.ps1 b/functions/Get-Tag.ps1
new file mode 100644
index 0000000..b208b9b
--- /dev/null
+++ b/functions/Get-Tag.ps1
@@ -0,0 +1,57 @@
+
+function Get-Tag {
+
+    [OutputType([PSCustomObject])]
+    [CmdletBinding(DefaultParameterSetName = 'Default')]
+    Param (
+
+        # Unique Id of the ticket to get tags for
+        [Parameter(Mandatory = $true,
+            ParameterSetName = 'Ticket')]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $TicketId,
+
+        # Unique Id of the organization to get tags for
+        [Parameter(Mandatory = $true,
+            ParameterSetName = 'Org')]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $OrganizationId,
+
+        # Unique Id of the user to get tags for
+        [Parameter(Mandatory = $true,
+            ParameterSetName = 'User')]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $UserId,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    switch ($PSCMDlet.ParameterSetName) {
+        'Ticket' {
+            $path = "/api/v2/tickets/$TicketId/tags.json"
+        }
+
+        'Org' {
+            $path = "/api/v2/organizations/$OrganizationId/tags.json"
+        }
+
+        'User' {
+            $path = "/api/v2/users/$UserId/tags.json"
+        }
+
+        default {
+            $path = '/api/v2/tags.json'
+        }
+    }
+
+    $result = Invoke-Method -Context $Context -Path $path -Verbose:$VerbosePreference
+    $result | Select-Object -Expand 'tags'
+
+}
diff --git a/functions/Get-Ticket.ps1 b/functions/Get-Ticket.ps1
new file mode 100644
index 0000000..3e1cefb
--- /dev/null
+++ b/functions/Get-Ticket.ps1
@@ -0,0 +1,147 @@
+
+function Get-Ticket {
+
+    [OutputType([PSCustomObject])]
+    [CmdletBinding(DefaultParameterSetName = 'Default')]
+    Param (
+        # Unique Id of the ticket to retrieve
+        [Parameter(Mandatory = $true,
+            ParameterSetName = 'Id')]
+        [ValidateNotNullOrEmpty()]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64[]]
+        $Id,
+
+        # External Id of the ticket to retrieve
+        [Parameter(Mandatory = $true,
+            ParameterSetName = 'ExternalId')]
+        [ValidateNotNullOrEmpty()]
+        [String]
+        $ExternalId,
+
+        # Unique Id of the user associated with this ticket.
+        [Parameter(Mandatory = $true,
+            ParameterSetName = 'Requested')]
+        [Parameter(Mandatory = $true,
+            ParameterSetName = 'Assigned')]
+        [Parameter(Mandatory = $true,
+            ParameterSetName = 'CCd')]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $UserId,
+
+        # Retrieve only tickets requested by user specified by `-UserId`
+        [Parameter(Mandatory = $true,
+            ParameterSetName = 'Requested')]
+        [Switch]
+        $Requested,
+
+        # Retrieve only tickets assigned to user specified by `-UserId`
+        [Parameter(Mandatory = $true,
+            ParameterSetName = 'Assigned')]
+        [Switch]
+        $Assigned,
+
+        # Retrieve only tickets with the user specified by `-UserId` CC'd
+        [Parameter(Mandatory = $true,
+            ParameterSetName = 'CCd')]
+        [Switch]
+        $CCd,
+
+        # UniqueId of the Organization to retrieve tickets for
+        [Parameter(Mandatory = $true,
+            ParameterSetName = 'OrgId')]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $OrganizationId,
+
+        # Only retrieve recently viewed by the logged in user
+        [Parameter(Mandatory = $true,
+            ParameterSetName = 'Recent')]
+        [Switch]
+        $Recent,
+
+        # Entities to sideload in the request
+        [Parameter(Mandatory = $false)]
+        [ValidateNotNullOrEmpty()]
+        [ValidateSet(
+            'comment_count',
+            'dates',
+            'groups',
+            'incident_counts',
+            'last_audits',
+            'metric_events',
+            'metric_sets',
+            'organizations',
+            'sharing_agreements',
+            'slas',
+            'ticket_forms',
+            'users'
+        )]
+        [String[]]
+        $SideLoad,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    $key = 'tickets'
+
+    switch ($PSCmdlet.ParameterSetName) {
+        'Id' {
+            if ($Id.count -gt 1) {
+                $ids = $Id -join ','
+                $path = "/api/v2/tickets/show_many.json?ids=$ids"
+            } else {
+                $path = "/api/v2/tickets/$Id.json"
+                $key = 'ticket'
+            }
+        }
+
+        'ExternalId' {
+            $path = "/api/v2/tickets.json?external_id=$ExternalId"
+        }
+
+        'Requested' {
+            $path = "/api/v2/users/$UserId/tickets/requested.json"
+        }
+
+        'Assigned' {
+            $path = "/api/v2/users/$UserId/tickets/assigned.json"
+        }
+
+        'CCd' {
+            $path = "/api/v2/users/$UserId/tickets/ccd.json"
+        }
+
+        'OrgId' {
+            $path = "/api/v2/organizations/$OrganizationId/tickets.json"
+        }
+
+        'Recent' {
+            $path = '/api/v2/tickets/recent.json'
+        }
+
+        default {
+            $path = '/api/v2/tickets.json'
+        }
+    }
+
+    $params = @{
+        Context = $Context
+        Path    = $path
+        Verbose = $VerbosePreference
+    }
+
+    if ($PSBoundParameters.ContainsKey('SideLoad')) {
+        $params.SideLoad = $SideLoad
+    }
+
+    $result = Invoke-Method @params
+    $result | Select-Object -Expand $key
+
+}
+
diff --git a/functions/Get-TicketCollaborator.ps1 b/functions/Get-TicketCollaborator.ps1
new file mode 100644
index 0000000..356ce60
--- /dev/null
+++ b/functions/Get-TicketCollaborator.ps1
@@ -0,0 +1,26 @@
+
+function Get-TicketCollaborator {
+
+    [OutputType([PSCustomObject])]
+    [CMDletBinding()]
+    Param (
+
+        # Unique Id of the ticket to get Collaborators for
+        [Parameter(Mandatory = $true)]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $TicketId,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    $path = "/api/v2/tickets/$TicketId/collaborators.json"
+
+    $result = Invoke-Method -Context $Context -Path $path -Verbose:$VerbosePreference
+    $result
+
+}
diff --git a/functions/Get-TicketEmailCC.ps1 b/functions/Get-TicketEmailCC.ps1
new file mode 100644
index 0000000..96d5ddb
--- /dev/null
+++ b/functions/Get-TicketEmailCC.ps1
@@ -0,0 +1,26 @@
+
+function Get-TicketEmailCC {
+
+    [OutputType([PSCustomObject])]
+    [CMDletBinding()]
+    Param (
+
+        # Unique Id of the ticket to get email ccs for
+        [Parameter(Mandatory = $true)]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $TicketId,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    $path = "/api/v2/tickets/$TicketId/email_ccs"
+
+    $result = Invoke-Method -Context $Context -Path $path -Verbose:$VerbosePreference
+    $result
+
+}
diff --git a/functions/Get-TicketFollower.ps1 b/functions/Get-TicketFollower.ps1
new file mode 100644
index 0000000..284e8b5
--- /dev/null
+++ b/functions/Get-TicketFollower.ps1
@@ -0,0 +1,26 @@
+
+function Get-TicketFollower {
+
+    [OutputType([PSCustomObject])]
+    [CMDletBinding()]
+    Param (
+
+        # Unique Id of the ticket to get followers for
+        [Parameter(Mandatory = $true)]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $TicketId,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    $path = "/api/v2/tickets/$TicketId/followers"
+
+    $result = Invoke-Method -Context $Context -Path $path -Verbose:$VerbosePreference
+    $result
+
+}
diff --git a/functions/Get-TicketRelated.ps1 b/functions/Get-TicketRelated.ps1
new file mode 100644
index 0000000..894a2f9
--- /dev/null
+++ b/functions/Get-TicketRelated.ps1
@@ -0,0 +1,26 @@
+
+function Get-TicketRelated {
+
+    [OutputType([PSCustomObject])]
+    [CMDletBinding()]
+    Param (
+
+        # Unique Id of the ticket to get related information for
+        [Parameter(Mandatory = $true)]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $TicketId,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    $path = "/api/v2/tickets/$TicketId/related.json"
+
+    $result = Invoke-Method -Context $Context -Path $path -Verbose:$VerbosePreference
+    $result
+
+}
diff --git a/functions/Get-User.ps1 b/functions/Get-User.ps1
new file mode 100644
index 0000000..2d5f9a6
--- /dev/null
+++ b/functions/Get-User.ps1
@@ -0,0 +1,142 @@
+
+function Get-User {
+
+    [OutputType([PSCustomObject])]
+    [CmdletBinding(DefaultParameterSetName = 'Default')]
+    Param (
+
+        # Unique Id of the user to retrieve
+        [Parameter(Mandatory = $true,
+            ParameterSetName = 'Id')]
+        [ValidateNotNullOrEmpty()]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64[]]
+        $Id,
+
+        # External Id of the user to retrieve
+        [Parameter(Mandatory = $true,
+            ParameterSetName = 'ExternalId')]
+        [ValidateNotNullOrEmpty()]
+        [String[]]
+        $ExternalId,
+
+        # Unique Id of the group to retrieve users for
+        [Parameter(Mandatory = $true,
+            ParameterSetName = 'GroupId')]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $GroupId,
+
+        # Unique Id of the organization to retrieve users for
+        [Parameter(Mandatory = $true,
+            ParameterSetName = 'OrgId')]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $OrganizationId,
+
+        # Role to retrieve users for
+        [Parameter(Mandatory = $false,
+            ParameterSetName = 'GroupId')]
+        [Parameter(Mandatory = $false,
+            ParameterSetName = 'OrgId')]
+        [Parameter(Mandatory = $false,
+            ParameterSetName = 'Default')]
+        [ValidateSet('end-user', 'agent', 'admin')]
+        [String[]]
+        $Role,
+
+        # Unique Id of the custom role to retrieve users for
+        [Parameter(Mandatory = $false,
+            ParameterSetName = 'GroupId')]
+        [Parameter(Mandatory = $false,
+            ParameterSetName = 'OrgId')]
+        [Parameter(Mandatory = $false,
+            ParameterSetName = 'Default')]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $RoleId,
+
+        # Entities to sideload in the request
+        [Parameter(Mandatory = $false)]
+        [ValidateNotNullOrEmpty()]
+        [ValidateSet(
+            'abilities',
+            'groups',
+            'identities',
+            'open_ticket_count',
+            'organizations',
+            'roles'
+        )]
+        [String[]]
+        $SideLoad,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    $key = 'users'
+
+    switch ($PSCmdlet.ParameterSetName) {
+        'Id' {
+            if ($Id.count -gt 1) {
+                $ids = $Id -join ','
+                $path = "/api/v2/users/show_many.json?ids=$ids"
+            } else {
+                $path = "/api/v2/users/$Id.json"
+                $key = 'user'
+            }
+        }
+
+        'ExternalId' {
+            $ids = $ExternalId -join ','
+            $path = "/api/v2/users/show_many.json?external_ids=$ids"
+        }
+
+        'GroupId' {
+            $path = "/api/v2/groups/$GroupId/users.json"
+        }
+
+        'OrgId' {
+            $path = "/api/v2/organizations/$OrganizationId/users.json"
+        }
+
+        default {
+            $path = '/api/v2/users.json'
+        }
+    }
+
+    Write-Debug -Message "Path: $path"
+
+    if ($PSBoundParameters.ContainsKey('Role')) {
+        if ($Role.count -gt 1) {
+            $roles = $Role -join '&role[]='
+            $path += "?role[]=$roles"
+        } else {
+            $path += "?role=$Role"
+        }
+
+        Write-Debug -Message "Path: $path"
+    }
+
+    if ($PSBoundParameters.ContainsKey('RoleId')) {
+        $path += "?permission_set=$RoleId"
+        Write-Debug -Message "Path: $path"
+    }
+
+    $params = @{
+        Context = $Context
+        Path    = $path
+        Verbose = $VerbosePreference
+    }
+
+    if ($PSBoundParameters.ContainsKey('SideLoad')) {
+        $params.SideLoad = $SideLoad
+    }
+
+    $result = Invoke-Method @params
+    $result | Select-Object -Expand $key
+
+}
diff --git a/functions/Get-UserRelated.ps1 b/functions/Get-UserRelated.ps1
new file mode 100644
index 0000000..d353ff2
--- /dev/null
+++ b/functions/Get-UserRelated.ps1
@@ -0,0 +1,24 @@
+
+function Get-UserRelated {
+
+    [OutputType([PSCustomObject])]
+    [CMDletBinding()]
+    Param (
+
+        # Unique Id of the user to get related information for
+        [Parameter(Mandatory = $true)]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $UserId,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    $result = Invoke-Method -Path "/api/v2/users/$UserId/related.json" -Context $Context -Verbose:$VerbosePreference
+    $result
+
+}
diff --git a/functions/Hide-Comment.ps1 b/functions/Hide-Comment.ps1
new file mode 100644
index 0000000..bc9fbbb
--- /dev/null
+++ b/functions/Hide-Comment.ps1
@@ -0,0 +1,31 @@
+
+function Hide-Comment {
+
+    [OutputType([String])]
+    [CmdletBinding()]
+    Param (
+
+        # Unique Id of the comment to hide
+        [Parameter(Mandatory = $true)]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $Id,
+
+        # Unique Id of the ticket the comment resides on
+        [Parameter(Mandatory = $true)]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $TicketId,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    $path = "/api/v2/tickets/$TicketId/comments/$Id/make_private.json"
+
+    $result = Invoke-Method -Context $Context -Method 'Put' -Path $path -Verbose:$VerbosePreference
+    $result
+}
diff --git a/functions/Import-Ticket.ps1 b/functions/Import-Ticket.ps1
new file mode 100644
index 0000000..00ace99
--- /dev/null
+++ b/functions/Import-Ticket.ps1
@@ -0,0 +1,36 @@
+
+function Import-Ticket {
+
+    [OutputType([PSCustomObject])]
+    [CMDletBinding()]
+    Param (
+
+        # Full ticket object to import
+        [Parameter(Mandatory = $true)]
+        [ValidateNotNullOrEmpty()]
+        [PSCustomObject[]]
+        $Ticket,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    if ($Ticket.count -gt 1) {
+        $path = '/api/v2/imports/tickets/create_many.json'
+        $body = @{
+            tickets = $Ticket
+        }
+    } else {
+        $path = '/api/v2/imports/tickets.json'
+        $body = @{
+            ticket = $Ticket[0]
+        }
+    }
+
+    $result = Invoke-Method -Context $Context -Method 'Post' -Path $path -Body $body -Verbose:$VerbosePreference
+    $result
+
+}
diff --git a/functions/Invoke-Method.ps1 b/functions/Invoke-Method.ps1
new file mode 100644
index 0000000..5d112e2
--- /dev/null
+++ b/functions/Invoke-Method.ps1
@@ -0,0 +1,195 @@
+
+function Invoke-Method {
+
+    [OutputType([PSCustomObject])]
+    [CMDletBinding()]
+    Param (
+
+        # Rest Method
+        [Parameter(Mandatory = $false)]
+        [ValidateSet('Delete', 'Get', 'Post', 'Put')]
+        [String]
+        $Method = 'Get',
+
+        # Path of the rest request. eg `/Account`
+        [Parameter(Mandatory = $true)]
+        [ValidateNotNullOrEmpty()]
+        [String]
+        $Path,
+
+        # Body of the Rest call
+        [Parameter(Mandatory = $false)]
+        [PSCustomObject]
+        $Body,
+
+        # Content-Type of the body content
+        [Parameter(Mandatory = $false)]
+        [ValidateNotNullOrEmpty()]
+        [String]
+        $ContentType = 'application/json',
+
+        # Whether to perform pagination.
+        [Parameter(Mandatory = $false)]
+        [Boolean]
+        $Pagination = $true,
+
+        # Property to sort by. Set to `$null` for no sorting. Defaults to `created_at`
+        [Parameter(Mandatory = $false)]
+        [AllowEmptyString()]
+        [String]
+        $SortBy = 'created_at',
+
+        # Whether to retry when rate limited.
+        [Parameter(Mandatory = $false)]
+        [Boolean]
+        $Retry = $true,
+
+        # Entities to sideload in the request
+        [Parameter(Mandatory = $false)]
+        [ValidateNotNullOrEmpty()]
+        [ValidateSet(
+            'abilities',
+            'app_installation',
+            'brands',
+            'categories',
+            'comment_count',
+            'comment_events',
+            'dates',
+            'groups',
+            'identities',
+            'incident_counts',
+            'last_audits',
+            'metric_events',
+            'metric_sets',
+            'open_ticket_count',
+            'organizations',
+            'permissions',
+            'roles',
+            'sharing_agreements',
+            'slas',
+            'ticket_forms',
+            'tickets',
+            'usage_1h',
+            'usage_24h',
+            'usage_30d',
+            'usage_7d',
+            'users'
+        )]
+        [String[]]
+        $SideLoad,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+
+    )
+
+    # Determine the context
+    if ($null -eq $Context) {
+        if (Test-Path Variable:\Script:Context) {
+            $Context = $Script:Context
+        } else {
+            throw $Script:NotConnectedMessage
+        }
+    }
+
+    $params = @{
+        Method  = $Method
+        Headers = @{
+            Accept = 'application/json'
+        }
+    }
+
+    if ($PSVersionTable.PSEdition -eq 'Core') {
+        # PS Core added native Basic Auth support
+        $params.Credential = $Context.Credential
+        $params.Authentication = 'Basic'
+    } else {
+        # PS Desktop requires manual header creation. Basic auth is only supported by challenge.
+        $raw = '{0}/token:{1}' -f $Context.Credential.username, $Context.Credential.GetNetworkCredential().password
+        $bytes = [System.Text.Encoding]::Unicode.GetBytes($raw)
+        $encoded =[Convert]::ToBase64String($bytes)
+
+        $params.Headers.Authentication = "basic: $encoded"
+    }
+
+    if ($PSBoundParameters.ContainsKey('Body')) {
+        if ($ContentType -eq 'application/json') {
+            $params.Body = $Body | ConvertTo-Json -Compress
+        } else {
+            $params.Body = $Body
+        }
+        $params.ContentType = $ContentType
+    }
+
+    $uri = $Context.BaseUrl + $Path
+
+    while (-not [String]::IsNullOrEmpty($uri)) {
+        Write-Debug -Message "Uri before SortBy: $uri"
+        if (-not [String]::IsNullOrEmpty($SortBy) -and $uri -notmatch 'sort_by=') {
+            if ($uri -match '\?') {
+                $uri += "&sort_by=$SortBy"
+            } else {
+                $uri += "?sort_by=$SortBy"
+            }
+        }
+
+        Write-Debug -Message "Uri before Sideload: $uri"
+        if (-not [String]::IsNullOrEmpty($SideLoad) -and $uri -notmatch 'include=') {
+            $sideloadJoined = $SideLoad -join ','
+            if ($uri -match '\?') {
+                $uri += "&include=$sideloadJoined"
+            } else {
+                $uri += "?include=$sideloadJoined"
+            }
+        }
+
+        try {
+            $result = Invoke-RestMethod @params -Uri $uri -Verbose:$VerbosePreference
+            $result
+        } catch {
+            $errorRecord = $_
+        }
+
+        # 429 Too Many Requests - Exponential rety with jitter.
+        $multiplier = 1
+        while ($Retry -and (Test-Path -Path Variable:/errorRecord) -and $null -ne $errorRecord -and
+            $errorRecord.Exception -is [Microsoft.PowerShell.Commands.HttpResponseException] -and
+            $errorRecord.Exception.Response.StatusCode.value__ -eq 429) {
+
+            $retryAfter = $errorRecord.Exception.Response.Headers.RetryAfter.Delta.TotalSeconds
+            $retryAfter = 1
+
+            $multiplier *= 2
+            $jitter = Get-Random -Minimum 0.0 -Maximum 1.0
+
+            $sleep = $retryAfter + $multiplier + $jitter
+            Write-Warning -Message "Too many requests! Retrying after $sleep seconds."
+            Start-Sleep -Seconds $sleep
+
+            $errorRecord = $null
+            try {
+                $result = Invoke-RestMethod @params -Uri $uri -Verbose:$VerbosePreference
+                $result
+            } catch {
+                $errorRecord = $_
+            }
+        }
+
+        if ((Test-Path -Path Variable:\errorRecord) -and $null -ne $errorRecord) {
+            # Get just the message without stack trace, category info, or qualified error
+            $errorMessage = $errorRecord.ToString()
+            Get-PSCallStack | Foreach-Object { $errorMessage += "`n" + $_.Command + ': line ' + $_.ScriptLineNumber }
+            throw $errorMessage
+        }
+
+        $uri = $null
+        if ($Pagination) {
+            if ($null -ne $result -and (Get-Member -InputObject $result -Name 'next_page' -MemberType Properties)) {
+                $uri = $result.next_page
+            }
+        }
+    }
+}
diff --git a/functions/Merge-User.ps1 b/functions/Merge-User.ps1
new file mode 100644
index 0000000..a7179dd
--- /dev/null
+++ b/functions/Merge-User.ps1
@@ -0,0 +1,39 @@
+
+function Merge-User {
+
+    [OutputType([PSCustomObject])]
+    [CMDletBinding(SupportsShouldProcess = $true)]
+    Param (
+
+        # Unique Id of the user to merge
+        [Parameter(Mandatory = $true)]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $UserId,
+
+        # Unique Id of the user to merge into
+        [Parameter(Mandatory = $true)]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $TargetUserId,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    $path = "/api/v2/users/$UserId/merge.json"
+    $body = @{
+        user = @{
+            id = $TargerUserId
+        }
+    }
+
+    if ($PSCmdlet.ShouldProcess("$UserId => $TargetUserId", 'Merge user.')) {
+        $result = Invoke-Method -Context $Context -Method 'Put' -Path $path -Body $body -Verbose:$VerbosePreference
+        $result
+    }
+
+}
diff --git a/functions/New-Group.ps1 b/functions/New-Group.ps1
new file mode 100644
index 0000000..75209b3
--- /dev/null
+++ b/functions/New-Group.ps1
@@ -0,0 +1,33 @@
+
+function New-Group {
+
+    [OutputType([PSCustomObject])]
+    [CMDletBinding(SupportsShouldProcess = $true)]
+    Param (
+
+        # The name of the group.
+        [Parameter(Mandatory = $true)]
+        [ValidateNotNullOrEmpty()]
+        [String]
+        $Name,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    $path = '/api/v2/groups.json'
+    $body = @{
+        group = @{
+            name = $Name
+        }
+    }
+
+    if ($PSCmdlet.ShouldProcess($Name, 'Create Group')) {
+        $result = Invoke-Method -Context $Context -Method 'Post' -Path $path -Body $body -Verbose:$VerbosePreference
+        $result
+    }
+
+}
diff --git a/functions/New-GroupMembership.ps1 b/functions/New-GroupMembership.ps1
new file mode 100644
index 0000000..d894aac
--- /dev/null
+++ b/functions/New-GroupMembership.ps1
@@ -0,0 +1,46 @@
+
+function New-GroupMembership {
+
+    [OutputType([PSCustomObject])]
+    [CMDletBinding(SupportsShouldProcess = $true)]
+    Param (
+
+        # The id of an agent
+        [Parameter(Mandatory = $true)]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $UserId,
+
+        # The id of a group
+        [Parameter(Mandatory = $true)]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $GroupId,
+
+        # If true, tickets assigned directly to the agent will assume this membership's group.
+        [Parameter(Mandatory = $false)]
+        [Switch]
+        $Default,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    $path = 'api/v2/group_memberships.json'
+    $body = @{
+        group_membership = @{
+            user_id  = $UserId
+            group_id = $GroupId
+            default  = $Default
+        }
+    }
+
+    if ($PSCmdlet.ShouldProcess($UserId, "Assign to Group: $GroupId")) {
+        $result = Invoke-Method -Context $Context -Method 'Post' -Path $path -Body $body -Verbose:$VerbosePreference
+        $result
+    }
+
+}
diff --git a/functions/New-OrganizationMembership.ps1 b/functions/New-OrganizationMembership.ps1
new file mode 100644
index 0000000..d0cd174
--- /dev/null
+++ b/functions/New-OrganizationMembership.ps1
@@ -0,0 +1,46 @@
+
+function New-OrganizationMembership {
+
+    [OutputType([PSCustomObject])]
+    [CMDletBinding(SupportsShouldProcess = $true)]
+    Param (
+
+        # The ID of the user for whom this memberships belongs
+        [Parameter(Mandatory = $true)]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $UserId,
+
+        # he ID of the organization associated with this user, in this membership
+        [Parameter(Mandatory = $true)]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $OrganizationId,
+
+        # Denotes whether this is the default organization membership for the user.
+        [Parameter(Mandatory = $false)]
+        [Switch]
+        $Default,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    $path = 'api/v2/group_memberships.json'
+    $body = @{
+        group_membership = @{
+            user_id         = $UserId
+            organization_id = $OrganizationId
+            default         = $Default
+        }
+    }
+
+    if ($PSCmdlet.ShouldProcess($UserId, "Assign to Org: $OrganizationId")) {
+        $result = Invoke-Method -Context $Context -Method 'Post' -Path $path -Body $body -Verbose:$VerbosePreference
+        $result
+    }
+
+}
diff --git a/functions/Remove-Attachment.ps1 b/functions/Remove-Attachment.ps1
new file mode 100644
index 0000000..73f314e
--- /dev/null
+++ b/functions/Remove-Attachment.ps1
@@ -0,0 +1,28 @@
+
+function Remove-Attachment {
+
+    [OutputType([PSCustomObject])]
+    [CmdletBinding(SupportsShouldProcess = $true)]
+    Param (
+
+        # Unique token of the attachment to delete
+        [Parameter(Mandatory = $true)]
+        [ValidateNotNullOrEmpty()]
+        [String]
+        $Token,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    $path = "/api/v2/uploads/$Token.json"
+
+    if ($PSCmdlet.ShouldProcess($Token, 'Delete attachment')) {
+        $result = Invoke-Method -Context $Context -Method 'Delete' -Path $path -Verbose:$VerbosePreference
+        $result
+    }
+
+}
diff --git a/functions/Remove-Group.ps1 b/functions/Remove-Group.ps1
new file mode 100644
index 0000000..3f2db37
--- /dev/null
+++ b/functions/Remove-Group.ps1
@@ -0,0 +1,28 @@
+
+function Remove-Group {
+
+    [OutputType([PSCustomObject])]
+    [CmdletBinding(SupportsShouldProcess = $true)]
+    Param (
+
+        # Unique Id of group to delete
+        [Parameter(Mandatory = $true)]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $Id,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    $path = "/api/v2/groups/$Id.json"
+
+    if ($PSCmdlet.ShouldProcess($Id, "Delete Group")) {
+        $result = Invoke-Method -Context $Context -Method 'Delete' -Path $path -Verbose:$VerbosePreference
+        $result
+    }
+
+}
diff --git a/functions/Remove-GroupMembership.ps1 b/functions/Remove-GroupMembership.ps1
new file mode 100644
index 0000000..61178ef
--- /dev/null
+++ b/functions/Remove-GroupMembership.ps1
@@ -0,0 +1,34 @@
+
+function Remove-GroupMembership {
+
+    [OutputType([PSCustomObject])]
+    [CmdletBinding(SupportsShouldProcess = $true)]
+    Param (
+
+        # Unique Id of group membership to remove
+        [Parameter(Mandatory = $true)]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [ValidateNotNullOrEmpty()]
+        [Int64[]]
+        $Id,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    if ($Id.count -gt 1) {
+        $ids = $Id -join ','
+        $path = "/api/v2/group_memberships/destroy_many.json?ids=$ids"
+    } else {
+        $path = "/api/v2/group_memberships/$Id.json"
+    }
+
+    if ($PSCmdlet.ShouldProcess("$Id", 'Delete Group Memberships')) {
+        $result = Invoke-Method -Context $Context -Method 'Delete' -Path $path -Verbose:$VerbosePreference
+        $result
+    }
+
+}
diff --git a/functions/Remove-OrganizationMembership.ps1 b/functions/Remove-OrganizationMembership.ps1
new file mode 100644
index 0000000..d1259f5
--- /dev/null
+++ b/functions/Remove-OrganizationMembership.ps1
@@ -0,0 +1,34 @@
+
+function Remove-OrganizationMembership {
+
+    [OutputType([PSCustomObject])]
+    [CmdletBinding(SupportsShouldProcess = $true)]
+    Param (
+
+        # Unique Id of the organization membership to remove
+        [Parameter(Mandatory = $true)]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [ValidateNotNullOrEmpty()]
+        [Int64[]]
+        $Id,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    if ($Id.count -gt 1) {
+        $ids = $Id -join ','
+        $path = "/api/v2/organization_memberships/destroy_many.json?ids=$ids"
+    } else {
+        $path = "/api/v2/organization_memberships/$Id.json"
+    }
+
+    if ($PSCmdlet.ShouldProcess("$Id", 'Delete Organization Membership')) {
+        $result = Invoke-Method -Context $Context -Method 'Delete' -Path $path -Verbose:$VerbosePreference
+        $result
+    }
+
+}
diff --git a/functions/Remove-SuspendedTicket.ps1 b/functions/Remove-SuspendedTicket.ps1
new file mode 100644
index 0000000..236290f
--- /dev/null
+++ b/functions/Remove-SuspendedTicket.ps1
@@ -0,0 +1,33 @@
+
+function Get-SuspendedTicket {
+
+    [OutputType([PSCustomObject])]
+    [CMDletBinding(SupportsShouldProcess = $true)]
+    Param (
+        # Unique Id of suspended ticket to retrieve
+        [Parameter(Mandatory = $false)]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [ValidateNotNullOrEmpty()]
+        [Int64[]]
+        $Id,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    if ($Id.Count -gt 1) {
+        $ids = $Id -join ','
+        $path = "/api/v2/suspended_tickets/destroy_many.json?ids=$ids"
+    } else {
+        $path = "/api/v2/suspended_tickets/$Id.json"
+    }
+
+    if ($PSCmdlet.ShouldProcess("$Id", 'Delete Suspended Ticket')) {
+        $result = Invoke-Method -Context $Context -Method 'Delete' -Path $path -Verbose:$VerbosePreference
+        $result
+    }
+
+}
diff --git a/functions/Remove-Tag.ps1 b/functions/Remove-Tag.ps1
new file mode 100644
index 0000000..cf31a45
--- /dev/null
+++ b/functions/Remove-Tag.ps1
@@ -0,0 +1,64 @@
+
+function Remove-Tag {
+
+    [OutputType([PSCustomObject])]
+    [CmdletBinding(SupportsShouldProcess = $true)]
+    Param (
+
+        # Unique Id of the ticket to remove tags from
+        [Parameter(Mandatory = $true,
+            ParameterSetName = 'Ticket')]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $TicketId,
+
+        # Unique Id of the organization to remove tags from
+        [Parameter(Mandatory = $true,
+            ParameterSetName = 'Org')]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $OrganizationId,
+
+        # Unique Id of the user to remove tags from
+        [Parameter(Mandatory = $true,
+            ParameterSetName = 'User')]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $UserId,
+
+        # Tags to remove from the entity
+        [Parameter(Mandatory = $true)]
+        [String[]]
+        $Tag,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    switch ($PSCMDlet.ParameterSetName) {
+        'Ticket' {
+            $path = "/api/v2/tickets/$TicketId/tags.json"
+        }
+
+        'Org' {
+            $path = "/api/v2/organizations/$OrganizationId/tags.json"
+        }
+
+        'User' {
+            $path = "/api/v2/users/$UserId/tags.json"
+        }
+    }
+
+    $body = {
+        tags = $Tag
+    }
+
+    if ($PSCmdlet.ShouldProcess("$Tag", 'Remove Tag')) {
+        $result = Invoke-Method -Context $Context -Method 'Delete' -Path $path -Body $body -Verbose:$VerbosePreference
+        $result
+    }
+
+}
diff --git a/functions/Remove-Ticket.ps1 b/functions/Remove-Ticket.ps1
new file mode 100644
index 0000000..d73c48d
--- /dev/null
+++ b/functions/Remove-Ticket.ps1
@@ -0,0 +1,47 @@
+
+function Remove-Ticket {
+
+    [OutputType([PSCustomObject])]
+    [CMDletBinding(SupportsShouldProcess = $true)]
+    Param (
+
+        # Unique Id of the ticket to delete
+        [Parameter(Mandatory = $true)]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64[]]
+        $Id,
+
+        # Permanently deleted a deleted ticket
+        [Parameter(Mandatory = $false)]
+        [Switch]
+        $Permanent,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    if ($Id.Count -gt 1) {
+        $ids = $Id -join ','
+
+        if ($Permanent) {
+            $path = "/api/v2/deleted_tickets/destroy_many?ids=$ids"
+        } else {
+            $path = "/api/v2/tickets/destroy_many.json?ids=$ids"
+        }
+    } else {
+        if ($Permanent) {
+            $path = "/api/v2/deleted_tickets/$Id.json"
+        } else {
+            $path = "/api/v2/tickets/$Id.json"
+        }
+    }
+
+    if ($PSCmdlet.ShouldProcess("$Id", 'Delete Ticket')) {
+        $result = Invoke-Method -Context $Context -Method 'Delete' -Path $path -Verbose:$VerbosePreference
+        $result
+    }
+
+}
diff --git a/functions/Remove-User.ps1 b/functions/Remove-User.ps1
new file mode 100644
index 0000000..39bdf41
--- /dev/null
+++ b/functions/Remove-User.ps1
@@ -0,0 +1,37 @@
+
+function Remove-User {
+
+    [OutputType([PSCustomObject])]
+    [CMDletBinding(SupportsShouldProcess = $true)]
+    Param (
+
+        # Unique Id of the user to delete
+        [Parameter(Mandatory = $true)]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $UserId,
+
+        # Permanently delete soft deleted user
+        [Parameter(Mandatory = $false)]
+        [Switch]
+        $Permanent,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    if ($Permanent) {
+        $path = "/api/v2/deleted_users/$UserId.json"
+    } else {
+        $path = "/api/v2/users/$UserId.json"
+    }
+
+    if ($PSCmdlet.ShouldProcess($UserId, 'Delete User')) {
+        $result = Invoke-Method -Context $Context -Method 'Delete' -Path $path -Verbose:$VerbosePreference
+        $result
+    }
+
+}
diff --git a/functions/Restore-DeletedTicket.ps1 b/functions/Restore-DeletedTicket.ps1
new file mode 100644
index 0000000..5ca2219
--- /dev/null
+++ b/functions/Restore-DeletedTicket.ps1
@@ -0,0 +1,32 @@
+
+function Restore-DeletedTicket {
+
+    [OutputType([PSCustomObject])]
+    [CMDletBinding(SupportsShouldProcess = $true)]
+    Param (
+
+        # Unique Id of soft deleted ticket to restore
+        [Parameter(Mandatory = $true)]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64[]]
+        $Id,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    if ($Id.Count -gt 1) {
+        $ids = $Id -join ','
+        $path = "/api/v2/deleted_tickets/restore_many?ids=$ids"
+    } else {
+        $path = "/api/v2/deleted_tickets/$Id/restore.json"
+    }
+
+    if ($PSCmdlet.ShouldProcess("$Id", 'Restore deleted ticket.')) {
+        $result = Invoke-Method -Context $Context -Method 'Put' -Path $path -Verbose:$VerbosePreference
+        $result
+    }
+}
diff --git a/functions/Restore-SuspendedTicket.ps1 b/functions/Restore-SuspendedTicket.ps1
new file mode 100644
index 0000000..8853929
--- /dev/null
+++ b/functions/Restore-SuspendedTicket.ps1
@@ -0,0 +1,33 @@
+
+function Restore-SuspendedTicket {
+
+    [OutputType([PSCustomObject])]
+    [CMDletBinding(SupportsShouldProcess = $true)]
+    Param (
+        # Unique Id of suspended ticket to restore
+        [Parameter(Mandatory = $true)]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [ValidateNotNullOrEmpty()]
+        [Int64[]]
+        $Id,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    if ($Id.Count -gt 1) {
+        $ids = $Id -join ','
+        $path = "/api/v2/suspended_tickets/recover_many.json?ids=$ids"
+    } else {
+        $path = "/api/v2/suspended_tickets/$Id/recover.json"
+    }
+
+    if ($PSCmdlet.ShouldProcess("$Id", 'Restore suspended ticket.')) {
+        $result = Invoke-Method -Context $Context -Method 'Put' -Path $path -Verbose:$VerbosePreference
+        $result
+    }
+
+}
diff --git a/functions/Search-.ps1 b/functions/Search-.ps1
new file mode 100644
index 0000000..1707bb7
--- /dev/null
+++ b/functions/Search-.ps1
@@ -0,0 +1,34 @@
+
+function Search- {
+    <#
+    .Synopsis
+    Searches Zendesk.
+    .DESCRIPTION
+    Searches Zendesk using the universal search api.
+    .EXAMPLE
+    Search-Zendesk -Query 'type:user user@company.com'
+    #>
+    [OutputType([String])]
+    [CmdletBinding()]
+    Param (
+        # Zendesk Search Query
+        [Parameter(Mandatory = $true)]
+        [ValidateNotNullOrEmpty()]
+        [String]
+        $Query,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    Write-Debug -Message "Query: $Query"
+    $Query = [uri]::EscapeDataString($Query)
+    Write-Debug -Message "Escaped Query: $Query"
+
+    $results = Invoke-Method -Context $Context -Path "/api/v2/search.json?query=$Query"
+    $results.results
+
+}
diff --git a/functions/Set-Tag.ps1 b/functions/Set-Tag.ps1
new file mode 100644
index 0000000..3fe8da2
--- /dev/null
+++ b/functions/Set-Tag.ps1
@@ -0,0 +1,64 @@
+
+function Set-Tag {
+
+    [OutputType([PSCustomObject])]
+    [CmdletBinding(DefaultParameterSetName = 'Default', SupportsShouldProcess = $true)]
+    Param (
+
+        # Unique Id of ticket to set tags for
+        [Parameter(Mandatory = $true,
+            ParameterSetName = 'Ticket')]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $TicketId,
+
+        # Unique Id of organization to set tags for
+        [Parameter(Mandatory = $true,
+            ParameterSetName = 'Org')]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $OrganizationId,
+
+        # Unique Id of user to set tags for
+        [Parameter(Mandatory = $true,
+            ParameterSetName = 'User')]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $UserId,
+
+        # Tags to set for the entity. Existing tags not included here will be removed.
+        [Parameter(Mandatory = $true)]
+        [String[]]
+        $Tag,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    switch ($PSCMDlet.ParameterSetName) {
+        'Ticket' {
+            $path = "/api/v2/tickets/$TicketId/tags.json"
+        }
+
+        'Org' {
+            $path = "/api/v2/organizations/$OrganizationId/tags.json"
+        }
+
+        'User' {
+            $path = "/api/v2/users/$UserId/tags.json"
+        }
+    }
+
+    $body = {
+        tags = $Tag
+    }
+
+    if ($PSCmdlet.ShouldProcess("$TicketId$OrganizationId$UserId", "Set Tags: $Tag")) {
+        $result = Invoke-Method -Context $Context -Method 'POST' -Path $path -Body $body -Verbose:$VerbosePreference
+        $result
+    }
+
+}
diff --git a/functions/Set-User.ps1 b/functions/Set-User.ps1
new file mode 100644
index 0000000..8f50131
--- /dev/null
+++ b/functions/Set-User.ps1
@@ -0,0 +1,248 @@
+
+function Set-User {
+
+    [OutputType([PSCustomObject])]
+    [CMDletBinding(SupportsShouldProcess = $true)]
+    Param (
+
+        # User Object to set
+        [Parameter(Mandatory = $true,
+            ParameterSetName = 'Object')]
+        [ValidateNotNullOrEmpty()]
+        [PSCustomObject[]]
+        $User,
+
+        # The user's primary email address.
+        [Parameter(Mandatory = $true,
+            ParameterSetName = 'Properties')]
+        [ValidatePattern('@')]
+        [String]
+        $Email,
+
+        # The user's name.
+        [Parameter(Mandatory = $false,
+            ParameterSetName = 'Properties')]
+        [ValidateNotNullOrEmpty()]
+        [String]
+        $Name,
+
+        # An alias displayed to end users.
+        [Parameter(Mandatory = $false,
+            ParameterSetName = 'Properties')]
+        [ValidateNotNullOrEmpty()]
+        [String]
+        $Alias,
+
+        # A custom role if the user is an agent on the Enterprise plan.
+        [Parameter(Mandatory = $false,
+            ParameterSetName = 'Properties')]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $CustomRoleId,
+
+        # Any details you want to store about the user.
+        [Parameter(Mandatory = $false,
+            ParameterSetName = 'Properties')]
+        [ValidateNotNullOrEmpty()]
+        [String]
+        $Details,
+
+        # A unique identifier from another system.
+        [Parameter(Mandatory = $false,
+            ParameterSetName = 'Properties')]
+        [ValidateNotNullOrEmpty()]
+        [String]
+        $ExternalId,
+
+        # The user's locale.
+        [Parameter(Mandatory = $false,
+            ParameterSetName = 'Properties')]
+        [ValidateNotNullOrEmpty()]
+        [String]
+        $Locale,
+
+        # The user's language identifier.
+        [Parameter(Mandatory = $false,
+            ParameterSetName = 'Properties')]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $LocaleId,
+
+        # Designates whether the user has forum moderation capabilities.
+        [Parameter(Mandatory = $false,
+            ParameterSetName = 'Properties')]
+        [boolean]
+        $Moderator,
+
+        # Any notes you want to store about the user.
+        [Parameter(Mandatory = $false,
+            ParameterSetName = 'Properties')]
+        [ValidateNotNullOrEmpty()]
+        [String]
+        $Notes,
+
+        # If the user can only create private comments.
+        [Parameter(Mandatory = $false,
+            ParameterSetName = 'Properties')]
+        [Boolean]
+        $OnlyPrivateComments,
+
+        # The id of the user's organization.
+        [Parameter(Mandatory = $false,
+            ParameterSetName = 'Properties')]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $OrganizationId,
+
+        # The id of the user's default group.
+        [Parameter(Mandatory = $false,
+            ParameterSetName = 'Properties')]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $DefaultGroupId,
+
+        # The user's primary phone number.
+        [Parameter(Mandatory = $false,
+            ParameterSetName = 'Properties')]
+        [ValidateNotNullOrEmpty()]
+        [String]
+        $Phone,
+
+        # The user's profile picture represented as an Attachment object.
+        [Parameter(Mandatory = $false,
+            ParameterSetName = 'Properties')]
+        [ValidateNotNullOrEmpty()]
+        [PSCustomObject]
+        $Photo,
+
+        # If the agent has any restrictions.
+        [Parameter(Mandatory = $false,
+            ParameterSetName = 'Properties')]
+        [Boolean]
+        $RestrictedAgent,
+
+        # The user's role.
+        [Parameter(Mandatory = $false,
+            ParameterSetName = 'Properties')]
+        [ValidateNotNullOrEmpty()]
+        [String]
+        $Role,
+
+        # The user's signature.
+        [Parameter(Mandatory = $false,
+            ParameterSetName = 'Properties')]
+        [ValidateNotNullOrEmpty()]
+        [String]
+        $Signature,
+
+        # If the agent is suspended.
+        [Parameter(Mandatory = $false,
+            ParameterSetName = 'Properties')]
+        [Boolean]
+        $Suspended,
+
+        # The user's tags.
+        [Parameter(Mandatory = $false,
+            ParameterSetName = 'Properties')]
+        [ValidateNotNullOrEmpty()]
+        [String[]]
+        $Tags,
+
+        # Specifies which tickets the user has access to.
+        [Parameter(Mandatory = $false,
+            ParameterSetName = 'Properties')]
+        [ValidateNotNullOrEmpty()]
+        [String]
+        $TicketRestriction,
+
+        # The user's time zone.
+        [Parameter(Mandatory = $false,
+            ParameterSetName = 'Properties')]
+        [ValidateNotNullOrEmpty()]
+        [String]
+        $TimeZone,
+
+        # Values of custom fields in the user's profile.
+        [Parameter(Mandatory = $false,
+            ParameterSetName = 'Properties')]
+        [HashTable]
+        $UserFields,
+
+        # The user's primary identity is verified or not.
+        [Parameter(Mandatory = $false,
+            ParameterSetName = 'Properties')]
+        [Boolean]
+        $Verified,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    if ($PSCmdlet.ParameterSetName -eq 'Properties') {
+
+        $path = '/api/v2/users/create_or_update.json'
+        $body = @{
+            user = @{
+                email = $Email
+            }
+        }
+
+        $map = @{
+            name                  = 'Name'
+            alias                 = 'Alias'
+            custom_role_id        = 'CustomRoleId'
+            details               = 'Details'
+            external_id           = 'ExternalId'
+            locale                = 'Locale'
+            locale_id             = 'LocaleId'
+            moderator             = 'Moderator'
+            notes                 = 'Notes'
+            only_private_comments = 'OnlyPrivateComments'
+            organization_id       = 'OrganizationId'
+            default_group_id      = 'DefaultGroupId'
+            phone                 = 'Phone'
+            photo                 = 'Photo'
+            restricted_agent      = 'RestrictedAgent'
+            role                  = 'Role'
+            signature             = 'Signature'
+            suspended             = 'Suspended'
+            tags                  = 'Tags'
+            ticket_restriction    = 'TicketRestriction'
+            time_zone             = 'TimeZone'
+            user_fields           = 'UserFields'
+            verified              = 'Verified'
+        }
+
+        foreach ($item in $map) {
+            $property = $item.key
+            $parameter = $item.value
+            if ($PSBoundParameters.ContainsKey($parameter)) {
+                $body.user[$property] = $PSBoundParameters.$parameter
+            }
+        }
+
+    } else {
+
+        if ($User.count -gt 1) {
+            $path = '/api/v2/users/create_or_update_many.json'
+            $body = @{
+                users = $User
+            }
+        } else {
+            $path = '/api/v2/users/create_or_update.json'
+            $body = @{
+                user = $User[0]
+            }
+        }
+
+    }
+
+    if ($PSCmdlet.ShouldProcess('Set Users')) {
+        $result = Invoke-Method -Context $Context -Method 'Post' -Path $path -Body $body -Verbose:$VerbosePreference
+        $result
+    }
+
+}
diff --git a/functions/Test-Connection.ps1 b/functions/Test-Connection.ps1
new file mode 100644
index 0000000..2c12b4c
--- /dev/null
+++ b/functions/Test-Connection.ps1
@@ -0,0 +1,26 @@
+
+function Test-Connection {
+    <#
+    .Synopsis
+    Checks that api credentials have been stored.
+    .DESCRIPTION
+    Checks that api credentials have been stored.
+    .EXAMPLE
+    if (Test-ZendeskConnection) {
+        Search-Zendesk @searchParams
+    }
+    #>
+    [OutputType([Bool])]
+    [CmdletBinding()]
+    Param(
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    $null = Get-AuthenticatedUser -Context $Context
+    $true
+
+}
diff --git a/functions/Update-Group.ps1 b/functions/Update-Group.ps1
new file mode 100644
index 0000000..5d08cf4
--- /dev/null
+++ b/functions/Update-Group.ps1
@@ -0,0 +1,40 @@
+
+function Update-Group {
+
+    [OutputType([PSCustomObject])]
+    [CMDletBinding(SupportsShouldProcess = $true)]
+    Param (
+
+        # Unique Id of the group to update
+        [Parameter(Mandatory = $true)]
+        [ValidateRange(1, [Int64]::MaxValue)]
+        [Int64]
+        $Id,
+
+        # The new name of the group.
+        [Parameter(Mandatory = $true,
+            ParameterSetName = 'Properties')]
+        [ValidateNotNullOrEmpty()]
+        [String]
+        $Name,
+
+        # Zendesk Connection Context from `Get-ZendeskConnection`
+        [Parameter(Mandatory = $false)]
+        [PSTypeName('ZendeskContext')]
+        [PSCustomObject]
+        $Context = $null
+    )
+
+    $path = "/api/v2/groups/$Id.json"
+    $body = @{
+        group = @{
+            name = $Name
+        }
+    }
+
+    if ($PSCmdlet.ShouldProcess($Id, 'Update Group Name.')) {
+        $result = Invoke-Method -Context $Context -Method 'Put' -Path $path -Body $body -Verbose:$VerbosePreference
+        $result
+    }
+
+}
diff --git a/tests/Invoke-Method.tests.ps1 b/tests/Invoke-Method.tests.ps1
new file mode 100644
index 0000000..59d49f0
--- /dev/null
+++ b/tests/Invoke-Method.tests.ps1
@@ -0,0 +1,226 @@
+
+Import-Module "$PSScriptRoot/../PwshZendesk.psm1" -Force
+
+Describe 'Invoke-Method' {
+
+    InModuleScope PwshZendesk {
+
+        $IRM = Get-Command -Name 'Invoke-RestMethod' -Module 'Microsoft.PowerShell.Utility'
+
+        $context = @{
+            Organization = 'httpbin'
+            BaseUrl      = 'https://httpbin.org'
+            Credential   = [System.Management.Automation.PSCredential]::New("email", ('api-key' | ConvertTo-SecureString -AsPlainText -Force))
+        }
+        $context | Add-Member -TypeName 'ZendeskContext'
+
+        # Needs to run before Invoke-RestMethod is mocked.
+        It 'Passes the creds using Basic Auth' {
+            { Invoke-Method -Context $context -Path '/basic-auth/email/api-key' } | Should -Not -Throw
+            { Invoke-Method -Context $context -Path '/basic-auth/different/creds' } | Should -Throw
+        }
+
+        $context = @{
+            Organization = 'company'
+            BaseUrl      = 'https://company.testdesk.com'
+            Credential   = [System.Management.Automation.PSCredential]::New("email", ('api-key' | ConvertTo-SecureString -AsPlainText -Force))
+        }
+        $context | Add-Member -TypeName 'ZendeskContext'
+
+        Mock -ModuleName PwshZendesk Invoke-RestMethod {}
+
+        It 'Uses the baseUrl' {
+            Invoke-Method -Context $context -Path '/'
+            Assert-MockCalled Invoke-RestMethod -Exactly 1 -ParameterFilter {$Uri -match 'https://company.testdesk.com'} -Scope It
+        }
+
+        It 'Uses the path' {
+            Invoke-Method -Context $context -Path '/Thing'
+            Assert-MockCalled Invoke-RestMethod -Exactly 1 -ParameterFilter {$Uri -match '/Thing'} -Scope It
+        }
+
+        It 'Accepts JSON' {
+            Invoke-Method -Context $context -Path '/'
+            Assert-MockCalled Invoke-RestMethod -Exactly 1 -ParameterFilter {
+                $Headers.ContainsKey('Accept') -and $Headers.Accept -match 'application\/json'
+            } -Scope It
+        }
+
+        It 'Cascades Verbose' {
+            Invoke-Method -Context $context -Path '/'
+            Assert-MockCalled Invoke-RestMethod -Exactly 1 -ParameterFilter { -not $VerbosePreference } -Scope It
+
+            Invoke-Method -Context $context -Path '/' -Verbose
+            Assert-MockCalled Invoke-RestMethod -Exactly 1 -ParameterFilter { $VerbosePreference } -Scope It
+
+            Invoke-Method -Context $context -Path '/' -Verbose:$false
+            Assert-MockCalled Invoke-RestMethod -Exactly 2 -ParameterFilter { -not $VerbosePreference } -Scope It
+        }
+
+        It 'Default Method: Get' {
+            Invoke-Method -Context $context -Path '/'
+            Assert-MockCalled Invoke-RestMethod -Exactly 1 -ParameterFilter {$Method -eq 'Get'} -Scope It
+        }
+
+        It 'Explicit Method: Get' {
+            Invoke-Method -Context $context -Method 'Get' -Path '/'
+            Assert-MockCalled Invoke-RestMethod -Exactly 1 -ParameterFilter {$Method -eq 'Get'} -Scope It
+        }
+
+        It 'Explicit Method: Post' {
+            Invoke-Method -Context $context -Method 'Post' -Path '/'
+            Assert-MockCalled Invoke-RestMethod -Exactly 1 -ParameterFilter {$Method -eq 'Post'} -Scope It
+        }
+
+        It 'Explicit Method: Put' {
+            Invoke-Method -Context $context -Method 'Put' -Path '/'
+            Assert-MockCalled Invoke-RestMethod -Exactly 1 -ParameterFilter {$Method -eq 'Put'} -Scope It
+        }
+
+        It 'Explicit Method: Delete' {
+            Invoke-Method -Context $context -Method 'Delete' -Path '/'
+            Assert-MockCalled Invoke-RestMethod -Exactly 1 -ParameterFilter {$Method -eq 'Delete'} -Scope It
+        }
+
+        It 'Throws with no connection' {
+            { Invoke-Method -Path '/' } | Should -Throw
+        }
+
+        It 'Throws with no invalid connection type' {
+            { Invoke-Method -Path '/' -Context [PSCustomObject]@{
+                Organization = 'company'
+                BaseUrl = 'https://company.testdesk.com'
+                Credential = $null
+            } } | Should -Throw
+        }
+
+        It 'Converts Body to JSON by default' {
+            Invoke-Method -Context $context -Method 'Post' -Path '/' -Body ([PSCustomObject]@{A=1; B=2; C=3})
+            Assert-MockCalled Invoke-RestMethod -Exactly 1 -ParameterFilter {$Body -eq '{"A":1,"B":2,"C":3}'} -Scope It
+            Assert-MockCalled Invoke-RestMethod -Exactly 1 -ParameterFilter {$ContentType -eq 'application/json'} -Scope It
+        }
+
+        It 'Leaves Body alone with explicit ContentType' {
+            Invoke-Method -Context $context -Method 'Post' -Path '/' -Body 'The Body' -ContentType 'text/plain'
+            Assert-MockCalled Invoke-RestMethod -Exactly 1 -ParameterFilter {$Body -eq 'The Body'} -Scope It
+            Assert-MockCalled Invoke-RestMethod -Exactly 1 -ParameterFilter {$ContentType -eq 'text/plain'} -Scope It
+        }
+
+        It 'Sorts by created_at by default' {
+            Invoke-Method -Context $context -Path '/'
+            Assert-MockCalled Invoke-RestMethod -Exactly 1 -ParameterFilter {$Uri -match 'sort_by=created_at'} -Scope It
+        }
+
+        It 'Sorts by any supplied property' {
+            Invoke-Method -Context $context -Path '/' -SortBy 'name'
+            Assert-MockCalled Invoke-RestMethod -Exactly 1 -ParameterFilter {$Uri -match 'sort_by=name'} -Scope It
+        }
+
+        It 'Doesn;t sort when SortBy is set to $null' {
+            Invoke-Method -Context $context -Path '/' -SortBy $null
+            Assert-MockCalled Invoke-RestMethod -Exactly 1 -ParameterFilter {$Uri -notmatch 'sort_by'} -Scope It
+        }
+
+        It 'Can sort if path includes query variables' {
+            Invoke-Method -Context $context -Path '/thing?name=jim'
+            Assert-MockCalled Invoke-RestMethod -Exactly 1 -ParameterFilter {$Uri -match '\?name=jim&sort_by'} -Scope It
+        }
+
+        Mock -ModuleName PwshZendesk Invoke-RestMethod { & $IRM 'httpstat.us/404' }
+
+        It 'Throws on a 404' {
+            { Invoke-Method -Context $context -Path '/' } | Should -Throw
+        }
+
+        It 'Passes on 404 error message' {
+            try { Invoke-Method -Context $context -Path '/' } catch { $E = $_}
+            $E | Should -Match '404 \(Not Found\)'
+        }
+
+        Mock -ModuleName PwshZendesk Invoke-RestMethod { & $IRM 'httpstat.us/400' }
+
+        It 'Passes on 400 error message' {
+            try { Invoke-Method -Context $context -Path '/' } catch { $E = $_}
+            $E | Should -Match '400 \(Bad Request\)'
+        }
+
+        Mock -ModuleName PwshZendesk Invoke-RestMethod { & $IRM 'httpstat.us/500' }
+
+        It 'Passes on 500 error message' {
+            try { Invoke-Method -Context $context -Path '/' } catch { $E = $_}
+            $E | Should -Match '500 \(Internal Server Error\)'
+        }
+
+        It 'Passes on Calling function' {
+            try { Invoke-Method -Context $context -Path '/' } catch { $E = $_}
+            $E | Should -Match '<ScriptBlock>: line 1\d\d'
+        }
+
+        Mock -ModuleName PwshZendesk Invoke-RestMethod {
+            if ($Uri -match 'idx=([0-9])') {
+                $idx = [int]$Matches[1]
+                if ($idx -eq 5) {
+                    [PSCustomObject]@{
+                        next_page = $null
+                    }
+                } else {
+                    $idx += 1
+                    [PSCustomObject]@{
+                        next_page = $Uri -replace 'idx=[0-9]', "idx=$idx"
+                    }
+                }
+            } else {
+                throw 'no idx'
+            }
+        }
+
+        It 'Pages by default' {
+            Invoke-Method -Context $context -Path '/Thing?idx=1'
+            Assert-MockCalled Invoke-RestMethod -Exactly 5 -Scope It
+        }
+
+        It 'Pages explicitly' {
+            Invoke-Method -Context $context -Path '/Thing?idx=1' -Pagination $true
+            Assert-MockCalled Invoke-RestMethod -Exactly 5 -Scope It
+        }
+
+        It 'Does not page when asked not to' {
+            Invoke-Method -Context $context -Path '/Thing?idx=1' -Pagination $false
+            Assert-MockCalled Invoke-RestMethod -Exactly 1 -Scope It
+        }
+
+        $Script:attempts = 0
+        Mock -ModuleName PwshZendesk Invoke-RestMethod {
+            $Script:attempts += 1
+            if ($Script:attempts -lt 3) {
+                & $IRM 'httpstat.us/429'
+            }
+        }
+
+        It 'Does not rety if requested not to' {
+            $Script:attempts = 0
+            { Invoke-Method -Context $context -Path '/' -Retry $false } | Should -Throw
+            Assert-MockCalled Invoke-RestMethod -Exactly 1 -Scope It
+        }
+
+        It 'Retries by default' {
+            $Script:attempts = 0
+            Invoke-Method -Context $context -Path '/' 3>$null
+            Assert-MockCalled Invoke-RestMethod -Exactly 3 -Scope It
+        }
+
+        It 'Retries explicitly' {
+            $Script:attempts = 0
+            Invoke-Method -Context $context -Path '/' -Retry $true 3>$null
+            Assert-MockCalled Invoke-RestMethod -Exactly 3 -Scope It
+        }
+
+        Mock -ModuleName PwshZendesk Invoke-RestMethod {}
+        $Script:Context = $context
+
+        It 'Uses the stored context' {
+            Invoke-Method -Path '/'
+            Assert-MockCalled Invoke-RestMethod -Exactly 1 -Scope It
+        }
+    }
+}
diff --git a/todo.md b/todo.md
new file mode 100644
index 0000000..c32ae6a
--- /dev/null
+++ b/todo.md
@@ -0,0 +1,11 @@
+
+- Comment Based Help
+- Default Parameter Sets
+- Tests
+- OAuth Support
+- Parameter Validation
+- OutputType
+- Attachment Uploads
+- Usage Compatability Check
+- PS Edition/Version Compatability Check
+- Ticket / User types?