WebCheckout API v0.1 Documentation

Sessions, Authentication, and Authorization

Users of the API are represented by one or more sessions. A session is created as soon as the user first connects to the API. Each session may or not be authenticated; once authenticated, it will represent a single user. Sessions will remain authenticated until the user explicitly logs out or the session times out.

Session Identification

In WebCheckout historically sessions have been maintained using browser cookies, so in practice all windows and tabs in the same browser share a single session. In order to allow for multiple session per browser we have added the "sessionid" argument. In the absense of this argument, the REST server will attempt to authenticate the user using a browser cookie.

Authentication

Any request, apart from an attempt to authenticate, submitted to the REST API prior to authenticating will recieve a message with the status unauthenticated.

POST rest/person/get

      {
          "oid": 1,
          "sessionid": ""
      }

HTTP 200

      {
          "apiVersion": "0.1",
          "session": null,
          "status": "unauthenticated",
          "notifications": null,
          "payload": {
              "message": "No session.",
              "motd": {
                  "PIR": "",
                  "WCO": null
              },
              "class": "REST-SERVER:NO-ACTIVE-SESSION"
          }
      }

Authentication is performed by posting the user name and password to the start command of the special namespace session.

POST rest/session/start

      {
          "userid": "wworker",
          "password": "window",
          "sessionid": ""
      }

HTTP 200

      {
          "apiVersion": "0.1",
          "session": "S-17897",
          "status": "ok",
          "notifications": null,
          "payload": {
              "id": "S-17897",
              "uuid": "54fa5f2e-649e-4638-a07c-c73d7046d71f",
              "agent": {
                  "_class": "person",
                  "oid": 2,
                  "name": "Window Worker",
                  "userid": "wworker"
              },
              "systemAuths": null,
              "roles": {
                  "patron": [
                      {
                          "_class": "location",
                          "oid": 1,
                          "name": "Main",
                          "organization": {
                              "_class": "organization",
                              "oid": 1,
                              "name": "Communications"
                          },
                          "description": null
                      }
                  ],
                  "operator": [
                      {
                          "_class": "location",
                          "oid": 2,
                          "name": "East",
                          "organization": {
                              "_class": "organization",
                              "oid": 1,
                              "name": "Communications"
                          },
                          "description": null
                      },
                      {
                          "_class": "location",
                          "oid": 1,
                          "name": "Main",
                          "organization": {
                              "_class": "organization",
                              "oid": 1,
                              "name": "Communications"
                          },
                          "description": null
                      },
                      {
                          "_class": "location",
                          "oid": 3,
                          "name": "West",
                          "organization": {
                              "_class": "organization",
                              "oid": 1,
                              "name": "Communications"
                          },
                          "description": null
                      }
                  ],
                  "employee": [
                      {
                          "_class": "organization",
                          "oid": 1,
                          "name": "Communications"
                      }
                  ],
                  "staff": null,
                  "manager": null
              },
              "checkoutCenter": null,
              "organization": null,
              "timezone": "America/Chicago",
              "locale": "en_US",
              "logoutUri": null,
              "timeout": 300,
              "expiration": 300,
              "twentyFourHourTime": false
          },
          "sessionid": "54fa5f2e-649e-4638-a07c-c73d7046d71f"
      }

If the API does not recognize the provided credentials the consumer is notified.

Roles and Session Location

Unfortunaly there have been several different uses of the word "location" in WebCheckout. In some cases it means a physical place, in others it means a distinct "Checkout Center", and others is is more abstract. Note that when we are talking about the type of an entity, location means Checkout Center, otherwise the term is intended to be more general.

In many cases, the proper functioning of the API requires the concept of a "Session Organization" or a "Session Checkout Center", collectively refered to as a session location. Only a few operations are available to the user before they have declared a session location. The session location may be changed at any time, however work in progress at the previous location may be lost when the location is changed.

Failure to set the session location before most API calls results in a unique error condition.

POST rest/resource/search

      {
          "sessionid": "54fa5f2e-649e-4638-a07c-c73d7046d71f"
      }

HTTP 200

      {
          "apiVersion": "0.1",
          "session": "S-17897",
          "status": "noOrganization",
          "notifications": null,
          "payload": {
              "message": "Command requires a session organization",
              "class": "REST-SERVER:NO-SESSION-ORGANIZATION"
          },
          "sessionid": "54fa5f2e-649e-4638-a07c-c73d7046d71f"
      }

The user may request the list of checkout centers where they are authorized to function as an operator, or where they are authorized to make allocations as a patron

POST rest/session/sessionRoles

      {
          "sessionid": "54fa5f2e-649e-4638-a07c-c73d7046d71f"
      }

HTTP 200

      {
          "apiVersion": "0.1",
          "session": "S-17897",
          "status": "ok",
          "notifications": null,
          "payload": {
              "patron": [
                  {
                      "_class": "location",
                      "oid": 1,
                      "name": "Main",
                      "organization": {
                          "_class": "organization",
                          "oid": 1,
                          "name": "Communications"
                      },
                      "description": null
                  }
              ],
              "operator": [
                  {
                      "_class": "location",
                      "oid": 2,
                      "name": "East",
                      "organization": {
                          "_class": "organization",
                          "oid": 1,
                          "name": "Communications"
                      },
                      "description": null
                  },
                  {
                      "_class": "location",
                      "oid": 1,
                      "name": "Main",
                      "organization": {
                          "_class": "organization",
                          "oid": 1,
                          "name": "Communications"
                      },
                      "description": null
                  },
                  {
                      "_class": "location",
                      "oid": 3,
                      "name": "West",
                      "organization": {
                          "_class": "organization",
                          "oid": 1,
                          "name": "Communications"
                      },
                      "description": null
                  }
              ],
              "employee": [
                  {
                      "_class": "organization",
                      "oid": 1,
                      "name": "Communications"
                  }
              ],
              "staff": null,
              "manager": null
          },
          "sessionid": "54fa5f2e-649e-4638-a07c-c73d7046d71f"
      }

The user may then set their session location to an organization...

POST rest/session/setSessionLocation

      {
          "organization": {
              "_class": "organization",
              "oid": 1
          },
          "sessionid": "54fa5f2e-649e-4638-a07c-c73d7046d71f"
      }

HTTP 200

      {
          "apiVersion": "0.1",
          "session": "S-17897",
          "status": "ok",
          "notifications": null,
          "payload": {
              "id": "S-17897",
              "uuid": "54fa5f2e-649e-4638-a07c-c73d7046d71f",
              "agent": {
                  "_class": "person",
                  "oid": 2,
                  "name": "Window Worker",
                  "userid": "wworker"
              },
              "systemAuths": [
                  [
                      "CIRCULATE",
                      "role"
                  ],
                  [
                      "EDIT-RESERVATIONS",
                      "role"
                  ],
                  [
                      "ADD-CONDITION-NOTES",
                      "role"
                  ]
              ],
              "roles": {
                  "patron": [
                      {
                          "_class": "location",
                          "oid": 1,
                          "name": "Main",
                          "organization": {
                              "_class": "organization",
                              "oid": 1,
                              "name": "Communications"
                          },
                          "description": null
                      }
                  ],
                  "operator": [
                      {
                          "_class": "location",
                          "oid": 2,
                          "name": "East",
                          "organization": {
                              "_class": "organization",
                              "oid": 1,
                              "name": "Communications"
                          },
                          "description": null
                      },
                      {
                          "_class": "location",
                          "oid": 1,
                          "name": "Main",
                          "organization": {
                              "_class": "organization",
                              "oid": 1,
                              "name": "Communications"
                          },
                          "description": null
                      },
                      {
                          "_class": "location",
                          "oid": 3,
                          "name": "West",
                          "organization": {
                              "_class": "organization",
                              "oid": 1,
                              "name": "Communications"
                          },
                          "description": null
                      }
                  ],
                  "employee": [
                      {
                          "_class": "organization",
                          "oid": 1,
                          "name": "Communications"
                      }
                  ],
                  "staff": null,
                  "manager": null
              },
              "checkoutCenter": null,
              "organization": {
                  "_class": "organization",
                  "oid": 1,
                  "name": "Communications"
              },
              "timezone": "America/Chicago",
              "locale": "en_US",
              "logoutUri": null,
              "timeout": 300,
              "expiration": 300,
              "twentyFourHourTime": false
          },
          "sessionid": "54fa5f2e-649e-4638-a07c-c73d7046d71f"
      }

... or to a checkout center

POST rest/session/setSessionLocation

      {
          "checkoutCenter": {
              "_class": "location",
              "oid": 1
          },
          "sessionid": "54fa5f2e-649e-4638-a07c-c73d7046d71f"
      }

HTTP 200

      {
          "apiVersion": "0.1",
          "session": "S-17897",
          "status": "ok",
          "notifications": null,
          "payload": {
              "id": "S-17897",
              "uuid": "54fa5f2e-649e-4638-a07c-c73d7046d71f",
              "agent": {
                  "_class": "person",
                  "oid": 2,
                  "name": "Window Worker",
                  "userid": "wworker"
              },
              "systemAuths": [
                  [
                      "CIRCULATE",
                      "role"
                  ],
                  [
                      "EDIT-RESERVATIONS",
                      "role"
                  ],
                  [
                      "ADD-CONDITION-NOTES",
                      "role"
                  ]
              ],
              "roles": {
                  "patron": [
                      {
                          "_class": "location",
                          "oid": 1,
                          "name": "Main",
                          "organization": {
                              "_class": "organization",
                              "oid": 1,
                              "name": "Communications"
                          },
                          "description": null
                      }
                  ],
                  "operator": [
                      {
                          "_class": "location",
                          "oid": 2,
                          "name": "East",
                          "organization": {
                              "_class": "organization",
                              "oid": 1,
                              "name": "Communications"
                          },
                          "description": null
                      },
                      {
                          "_class": "location",
                          "oid": 1,
                          "name": "Main",
                          "organization": {
                              "_class": "organization",
                              "oid": 1,
                              "name": "Communications"
                          },
                          "description": null
                      },
                      {
                          "_class": "location",
                          "oid": 3,
                          "name": "West",
                          "organization": {
                              "_class": "organization",
                              "oid": 1,
                              "name": "Communications"
                          },
                          "description": null
                      }
                  ],
                  "employee": [
                      {
                          "_class": "organization",
                          "oid": 1,
                          "name": "Communications"
                      }
                  ],
                  "staff": null,
                  "manager": null
              },
              "checkoutCenter": {
                  "_class": "location",
                  "oid": 1,
                  "name": "Main",
                  "organization": {
                      "_class": "organization",
                      "oid": 1,
                      "name": "Communications"
                  },
                  "description": null
              },
              "organization": {
                  "_class": "organization",
                  "oid": 1,
                  "name": "Communications"
              },
              "timezone": "America/Chicago",
              "locale": "en_US",
              "logoutUri": null,
              "timeout": 10800,
              "expiration": 10800,
              "twentyFourHourTime": false
          },
          "sessionid": "54fa5f2e-649e-4638-a07c-c73d7046d71f"
      }

Note that when setting the location to an organization, the session checkout center is null. When setting the location to a checkout center the session organization is the organization of the checkout center

Session Information

Information about the current session, including the session timezone, locale, and relevant information about the session agent.

Note that currentSession is an example of a POST operation with an empty payload assuming authentication is being performed using browser codes.

POST rest/session/currentSession

      {
          "sessionid": "54fa5f2e-649e-4638-a07c-c73d7046d71f"
      }

HTTP 200

      {
          "apiVersion": "0.1",
          "session": "S-17897",
          "status": "ok",
          "notifications": null,
          "payload": {
              "id": "S-17897",
              "uuid": "54fa5f2e-649e-4638-a07c-c73d7046d71f",
              "agent": {
                  "_class": "person",
                  "oid": 2,
                  "name": "Window Worker",
                  "userid": "wworker"
              },
              "systemAuths": [
                  [
                      "CIRCULATE",
                      "role"
                  ],
                  [
                      "EDIT-RESERVATIONS",
                      "role"
                  ],
                  [
                      "ADD-CONDITION-NOTES",
                      "role"
                  ]
              ],
              "roles": {
                  "patron": [
                      {
                          "_class": "location",
                          "oid": 1,
                          "name": "Main",
                          "organization": {
                              "_class": "organization",
                              "oid": 1,
                              "name": "Communications"
                          },
                          "description": null
                      }
                  ],
                  "operator": [
                      {
                          "_class": "location",
                          "oid": 2,
                          "name": "East",
                          "organization": {
                              "_class": "organization",
                              "oid": 1,
                              "name": "Communications"
                          },
                          "description": null
                      },
                      {
                          "_class": "location",
                          "oid": 1,
                          "name": "Main",
                          "organization": {
                              "_class": "organization",
                              "oid": 1,
                              "name": "Communications"
                          },
                          "description": null
                      },
                      {
                          "_class": "location",
                          "oid": 3,
                          "name": "West",
                          "organization": {
                              "_class": "organization",
                              "oid": 1,
                              "name": "Communications"
                          },
                          "description": null
                      }
                  ],
                  "employee": [
                      {
                          "_class": "organization",
                          "oid": 1,
                          "name": "Communications"
                      }
                  ],
                  "staff": null,
                  "manager": null
              },
              "checkoutCenter": {
                  "_class": "location",
                  "oid": 1,
                  "name": "Main",
                  "organization": {
                      "_class": "organization",
                      "oid": 1,
                      "name": "Communications"
                  },
                  "description": null
              },
              "organization": {
                  "_class": "organization",
                  "oid": 1,
                  "name": "Communications"
              },
              "timezone": "America/Chicago",
              "locale": "en_US",
              "logoutUri": null,
              "timeout": 10800,
              "expiration": 10800,
              "twentyFourHourTime": false
          },
          "sessionid": "54fa5f2e-649e-4638-a07c-c73d7046d71f"
      }
Timeouts

All API sessions time out after a certain period of inactivity based on the authenticated user and the sessions location.

The current timout and remaining inactiviy time can be retrieved by calling the session/timeLeft command. Most calls to the REST API will reset the session timeout with the expection of start, info, currentSession, timeLeft, and currentTime commands in the session namespace.

Authorizations

Every operation through the rest API apart from authentication requires the session have certain authorizations. Some operations may be performed beyond the authorization of the current session by way of "Pinning." In this mechanism the pin of a user with greater authorization is provided along with a command to authorize the action.

Authorizations are defined on commands and properties as a list, with each authorization applied in turn. The first authorization that "matches" is applied and all subsequent tests are skipped.

For security reasons this whole mechanism may change substanitally in the future.

Authorization Definitions

Authorizations to perform commands or to read or write properties come in one of three types; defaults, authorization tests, and system authorizations (sysauths.)

Default Authorizations

An authorization may be defined as "defaultAllow". This means that any propperly authenticated user of the application is authorized for this action. If, after all authorizations have been exhaustesd without a match, the authorization is denyed by default.

Authorization Tests

Authorization tests are determined based on the authenticated users relationship to the data being requested or manipulated. For example, an authenticated user has access to more information about their own person">person record than they do the person">person records of others. They are also able to manipulate allocation">allocations where they are the patron

Authorization tests come in two forms "ALLOW" and "DENY". If an "ALLOW" test matches the action is concidered authorized; if a "DENY" test matches the action immediatly is rejected.

An authorization test is represented in the JSON listing the name of the test, the arguments to which the test is applied, and a bti of documentation about the test.

	{
            "test": "ownReservation", 
            "testArguments": ["allocation"], 
            "testDocumentation": "True if the given allocation is a reservation (editable or no) and the session agent is the patron", 
            "type": "allow"
        }
System Authorizations

System authorizations, or sysauths, are granted to specific users based on their affiliation with organizations and apply to entities and actions under the control of that organization. A complete list of granted sysauths can be found in the details of the session returned by the session/start, session/setSessionLocation, and session/currentSession commands.

A typical sysauth definition lists the auths required and the entity (argument) to which the auths are applied.

	{
	    "auths": ["CIRCULATE"], 
	    "entity": "allocation", 
	    "type": "sysauth"
	}

sysauthNoPin identifies a sysauth which will not require Ident Pinning (see below).

        {
            "auths": ["CIRCULATE"], 
            "entity": "allocation", 
            "type": "sysauthNoPin"
        }

Some commands take a list of arguments which all have to be authorized. In these cases the entity value will be a list of an identifier used for each item, and the list argument that the auths will be applied to

        {
            "auths": ["CIRCULATE"], 
            "entity": [
                "item", 
                "items"
            ], 
            "type": "sysauthAll"
        }

In some cases the list of required authorizations will be generated at run time based on the supplied arguments. In this case the definition will list the command used to generate the list of sysauths, and some documentation about how that command functions.

	{
            "args": ["allocation"], 
            "command": "requiredCancelAuths", 
            "documentation": "Returns #!circulate #!edit-reservations and #!manage-pir-reservations if this allocation requires approval, #!circulate and #!edit-reservations otherwise", 
            "entity": "allocation", 
            "type": "sysauth"
        }

Please note that for properties, authorizations list the entity as "self", meaning the entity whos property is being read or written

        {
            "auths": [
                "OPERATOR"
            ], 
            "entity": "self", 
            "type": "sysauth"
        }
Authorization Failure

When an unauthorized action is performed, the API responds that the agent of the current session does not have the authorization to perform that operation.

POST rest/person/update

      {
          "oid": 1,
          "properties": {
              "barcode": "12345"
          },
          "sessionid": "54fa5f2e-649e-4638-a07c-c73d7046d71f"
      }

HTTP 200

      {
          "apiVersion": "0.1",
          "session": "S-17897",
          "status": "authorizationRequired",
          "notifications": null,
          "payload": "You're not authorized to manage people.",
          "sessionid": "54fa5f2e-649e-4638-a07c-c73d7046d71f"
      }
Pinning

The above request with the included pin argument will perform the operation with the permissions of the user with the given pin.

POST rest/person/update

      {
          "oid": 1,
          "properties": {
              "barcode": "12345"
          },
          "pin": "admin",
          "sessionid": "54fa5f2e-649e-4638-a07c-c73d7046d71f"
      }

HTTP 200

      {
          "apiVersion": "0.1",
          "session": "S-17897",
          "status": "ok",
          "notifications": null,
          "payload": {
              "_class": "person",
              "oid": 1,
              "name": "Daniel T. Pyne",
              "userid": "admin"
          },
          "sessionid": "54fa5f2e-649e-4638-a07c-c73d7046d71f"
      }
Pin Failure

If an incorrect pin, or a pin for a user that lacks the required authorization is provided, the API notifies the client of a authorization failure.

POST rest/person/update

      {
          "oid": 1,
          "properties": {
              "barcode": "12345"
          },
          "pin": "badPin",
          "sessionid": "54fa5f2e-649e-4638-a07c-c73d7046d71f"
      }

HTTP 200

      {
          "apiVersion": "0.1",
          "session": "S-17897",
          "status": "authorizationFailed",
          "notifications": null,
          "payload": "Invalid PIN",
          "sessionid": "54fa5f2e-649e-4638-a07c-c73d7046d71f"
      }
Ident Pinning

When one or more system authorizations is checked for a session, unless that session has the "BYPASS-PIN" authorization, the API will require that the user enter their pin to reconfirm the users identity.

POST rest/resource/update

      {
          "oid": 2140,
          "properties": {
              "conditionNote": "Scratched"
          },
          "sessionid": "54fa5f2e-649e-4638-a07c-c73d7046d71f"
      }

HTTP 200

      {
          "apiVersion": "0.1",
          "session": "S-17897",
          "status": "authorizationRequired",
          "notifications": null,
          "payload": "Please enter your PIN to confirm your identity.",
          "sessionid": "54fa5f2e-649e-4638-a07c-c73d7046d71f"
      }

Providing the pin of the session agent will complete the operation.

POST rest/resource/update

      {
          "oid": 2140,
          "properties": {
              "conditionNote": "Scratched"
          },
          "pin": "window",
          "sessionid": "54fa5f2e-649e-4638-a07c-c73d7046d71f"
      }

HTTP 200

      {
          "apiVersion": "0.1",
          "session": "S-17897",
          "status": "ok",
          "notifications": null,
          "payload": {
              "_class": "resource",
              "oid": 2140,
              "name": "Camera-01",
              "statusString": "Available"
          },
          "sessionid": "54fa5f2e-649e-4638-a07c-c73d7046d71f"
      }

An incorrect identity pin results in the following.

POST rest/resource/update

      {
          "oid": 2140,
          "properties": {
              "conditionNote": "Scratched"
          },
          "pin": "notMyPin",
          "sessionid": "54fa5f2e-649e-4638-a07c-c73d7046d71f"
      }

HTTP 200

      {
          "apiVersion": "0.1",
          "session": "S-17897",
          "status": "authorizationFailed",
          "notifications": null,
          "payload": "Invalid PIN",
          "sessionid": "54fa5f2e-649e-4638-a07c-c73d7046d71f"
      }
System Authorizations

Following is a current list of all system authorizations

"ADD-CONDITION-NOTES"

Change condition notes

"ALTER-SYSTEM"

Alter system configuration

"BACKUP-DATABASE"

Backup database

"BYPASS-PIN"

Bypass PIN entry

"CAN-IMPORT"

Import data

"CHANGE-RESOURCE-LOCATION"

Change resource Checkout Center

"CIRCULATE"

Perform checkouts and reservations

"EDIT-RESERVATIONS"

Modify existing reservations

"EMAIL-PATRONS"

Send email to patrons

"MANAGE-AUTHS"

Manage resource type authorizations

"MANAGE-BIBLIO"

Manage bibliographic records

"MANAGE-CATALOG"

Manage holdings

"MANAGE-DEPTS"

Manage departments

"MANAGE-EMPLOYEES"

Manage operators

"MANAGE-FINES"

Manage invoices and holds

"MANAGE-FREELANCERS"

Manage freelancers

"MANAGE-LOCATIONS"

Manage checkout centers and stations

"MANAGE-ORGANIZATION"

Manage organization

"MANAGE-ORGS"

Manage organizations

"MANAGE-PEOPLE"

Manage people

"MANAGE-PERSONNEL-SCHEDULING"

Manage personnel scheduling

"MANAGE-PIR-ACCESS"

Manage Patron Portal access

"MANAGE-PIR-RESERVATIONS"

Manage Patron Portal reservations

"MANAGE-RESOURCES"

Manage resources

"MANAGE-ROLES"

Manage roles

"MANAGE-RTYPES"

Manage resource types

"MANAGE-TICKETS"

Manage tickets

"MIN-RESERVATION-LEAD-TIME"

Override the minimum reservation lead time (time after creating a reservation before it may be scheduled for pickup)

"ONLINE-OFFLINE"

Take resources on/off line

"OPERATOR"

View operation level information

"OVERRIDE-ALLOCATION-LOCK"

Override allocation locks; may edit an allocation that is currently being edited by another person

"OVERRIDE-AUTHORIZATION"

Override authorization restrictions

"OVERRIDE-CHECKOUT-TO-SELF"

Override restriction on making checkouts or reservations to oneself

"OVERRIDE-FINE"

Override a patron's fine to allow checkouts

"OVERRIDE-FORBIDDEN-PICKUP-RETURN"

Override pickups or returns forbidden for a given day

"OVERRIDE-FREELANCER-UNFILLED-ROLES"

Override unfilled roles for events

"OVERRIDE-HOLD"

Override a patron's hold to allow checkouts

"OVERRIDE-INACTIVE-PATRON"

Override a patron's inactive status

"OVERRIDE-LATE-RESOURCES"

Override a patron's late returns to allow checkouts

"OVERRIDE-LOCATION-HOURS"

Operate outside of checkout center hours

"OVERRIDE-MAX-CHECKOUT"

Override limit on length of checkout or reservation

"OVERRIDE-MAX-RENEWALS"

Override limit on number of renewals for a checkout

"OVERRIDE-MAX-RESOURCES"

Override limit on number of resources in a checkout or reservation

"OVERRIDE-MAX-SIMULTANEOUS-CIRCULATION-EVENTS"

Override the maximum simultaneous circulation events limit

"OVERRIDE-MAX-UNSERIALIZED-QTY"

Override quantity of unserialized resources

"OVERRIDE-OPEN-CLOSE-INTERVALS"

Override minimum intervals between open or closing times and pickup or return times

"OVERRIDE-PATRON-MAX-RESERVATIONS"

Override maximum simultaneous allocations per patron

"OVERRIDE-RESERVATION-ADVANCE"

Override limit on advance time for reservations

"OVERRIDE-RESERVE-PAST"

Create reservations that start in the past

"OVERRIDE-RESOURCE-TYPE-LIMIT-PER-PATRON"

Override resource type limit per patron

"OVERRIDE-RESTRICTED-ORG"

Override non-membership in restricted organizations

"OVERRIDE-TURNAROUND"

Override turnaround time when checking out recently returned resources

"OVERRIDE-USAGE-LIMITS"

Override usage limits for resources

"PERFORM-INVENTORY"

Perform inventory

"SCHEDULE-FREELANCERS"

Schedule freelancers