Courier Invoice

API Overview

The purpose of this API is to give our clients direct access to their data so that they can better serve their clients. This API implements the project written by Maurits van der Schee released under the MIT License and hosted on GitHub at version 2.

F.A.Q.


Functions And Code Examples
PHP >= 7.0

countryFromAbbr call buildURI testResponse responseError webhookHandler invoiceCron

End Points

All non-numeric resources are collated utf8mb4_unicode_ci. The end points, resource names, and write permissions for the Courier Invoice API are as follows:

config contract_locations contract_runs c_run_schedule routes route_schedule route_tickets schedule dispatchers drivers clients o_clients schedule_override tickets invoices webhooks

configInformation relating to processing and displaying tickets and invoices.

Resource Data Type Read Only Null Default Description
Return To Top
config_index int(11)(AI) TRUE PRIMARY KEY index used for updating via API
user_index int(11) TRUE NO Foreign Key Constraint. References client_index of client 0 on the clients end point.
LogoFileName varchar(11) TRUE NO logo.* File upload script renames files to "logo" with the appropriate file extension.
CurrencySymbol varchar(8) FALSE NO ¤
WeightsMeasures tinyint(1) FALSE NO 0 0 = Imperial, 1 = Metric
InternationalAddressing tinyint(1) FALSE NO 0 0 = Hide country input and display, 1 = Show country input and display
TimeZone varchar(42) FALSE NO UTC Timezone string ex: America/North_Dakota/New_Salem
Supported Timezones
diPrice decimal(6,2) FALSE NO 0.00
OneHour float FALSE NO 1
TwoHour float FALSE NO 1
ThreeHour float FALSE NO 1
FourHour float FALSE NO 1
DeadRun float FALSE NO 0
DedicatedRunRate float FALSE NO 1
ApplyVAT tinyint(1) FALSE NO 0 bool. Indicates if value added tax options should be displayed.
DefaultTerms int(1) FALSE NO 1 Indicates the default terms for invoices. 1 = Due Upon Receipt, 2 = Net D, 3 = Net EOM D, 4 = A/B Net D.
DiscountRate decimal(4,2) FALSE NO 0.00 The percentage discount (A) offered for A/B Net D terms.
DiscountWindow int(3) FALSE NO 0 The number of days (B) a discount rate is offered for A/B Net D terms.
TermLength int(3) FALSE NO 0 The period of days (D) that terms are extended.
InvoiceBy int(1) FALSE NO 0 Date to invoice tickets by: 0 = ReceivedDate, 1 = ReadyDate, 2 = Compleation Date.
MaximumFee decimal(6,2) FALSE NO 0 A value of 0 (zero) indicates no maximum.
Geocoders text FALSE YES NULL
BaseTicketFee decimal(6,2) FALSE YES NULL
RangeIncrement tinyint(3) FALSE YES NULL
PriceIncrement float FALSE YES NULL
MaxRange decimal(7,2) FALSE YES NULL
RangeCenter varchar(23) FALSE YES NULL Latitude and Longitude of delivery range center. Ex: 41.2522201,-95.9822628

contract_locationsInformation relating to the locations for contract/repeating runs.

Resource Data Type Read Only Null Default Description
Return To Top
cloc_index int(11)(AI) TRUE PRIMARY KEY index used for updating via API
Client varchar(45) FALSE NO None
Department varchar(45) FALSE YES NULL
Contact varchar(45) FALSE YES NULL
Telephone varchar(20) FALSE YES NULL
Address1 varchar(45) FALSE NO None
Address2 varchar(45) FALSE NO None
Country varchar(2) FALSE NO None Country abbreviation generated with this function
Deleted tinyint(1) FALSE NO 0

contract_runsDetails for contract/repeating runs.

Resource Data Type Read Only Null Default Description
Return To Top
crun_index int(11)(AI) TRUE PRIMARY KEY index used for updating via API
RunNumber int(11) FALSE NO None This value should be confirmed unique before submission.
BillTo int(11) FALSE NO None References clients
pickup_id int(11) FALSE NO 1 Foreign Key Constraint. References cloc_index on the contract_locations endpoint. This value is returned when a location is succfully entered via the API, an array of values is returned if locations are batch entered.
dropoff_id int(11) FALSE NO 1 Foreign Key Constraint. References cloc_index on the contract_locations endpoint. This value is returned when a location is succfully entered via the API, an array of values is returned if locations are batch entered.
RoundTrip tinyint(1) FALSE NO 0 bool
pTime time FALSE YES NULL
dTime time FALSE YES NULL
d2Time time FALSE YES NULL
pSigReq tinyint(1) FALSE NO 0 Request signature on pick up.
dSigReq tinyint(1) FALSE NO 0 Request signature on delivery.
d2SigReq tinyint(1) FALSE NO 0 Request signature on return.
StartDate date FALSE NO None
LastCompleted date FALSE YES NULL Used for scheduling
DryIce tinyint(1) FALSE NO 0 bool
diWeight decimal(6,3) FALSE NO 0.000
Notes text FALSE YES NULL
PriceOverride tinyint(1) FALSE NO 0 bool
VATable tinyint(1) FALSE NO 0 bool. Is this ticket subject to VAT.
VATtype int(1) FALSE NO 0 0 = Not VAT-able, 1 = Standard, 2 = Reduced, 3 = Client Standard, 4 = Client Reduced, 5 = Zero-Rated, 6 = Exempt
VATableIce tinyint(1) FALSE NO 0 bool. Is the dry ice on this ticket subject to VAT.
VATtypeIce int(1) FALSE NO 0 0 = Not VAT-able, 1 = Standard, 2 = Reduced, 3 = Client Standard, 4 = Client Reduced, 5 = Zero-Rated, 6 = Exempt
TicketPrice decimal(6,2) FALSE Yes NULL

c_run_schedule Schedule information for contract runs

Resource Data Type Read Only Null Default Description
Return To Top
crs_index int(11)(AI) TRUE PRIMARY KEY index used for updating via API
crun_index int(11) FALSE FALSE 1 Foreign Key Constraint. References crun_index on the contract_runs end point.
schedule_index int(11) FALSE FALSE 1 Foreign Key Constraint. References the schedule end point.

routesDetails routes grouping contract runs.

Resource Data Type Read Only Null Default Description
Return To Top
route_index int(11)(AI) TRUE PRIMARY KEY index used for updating via API
RouteName varchar(40) FALSE No New Route User assigned value to identify routes individually.
driver_index int(11) FALSE No 0 Foreign Key Constraint. References driver_index on the drivers endpoint. 1 = Not Dispatched.
Overnight tinyint(1) FALSE No 0 bool. If 0 the route will be treated as though it begins in the morning and ends in the evening with early hours preceding later ones. If 1 the route will be treated as though it begins in the evening and ends in the morning with early hours following later ones.
LastDispatched datetime FALSE Yes null Date and time, Y-m-d h:i:s, that the route was last dispatched.
StartTime time FALSE Yes null The earliest time, hh:ii:ss, that the route can be dispatched.
StartDate date FALSE Yes null The first date to apply the route. Null should be interpreted as the first time the route is tested.

route_schedule Schedule information for routes

Resource Data Type Read Only Null Default Description
Return To Top
rts_index int(11)(AI) TRUE PRIMARY KEY index used for updating via API
route_index int(11) FALSE FALSE 1 Foreign Key Constraint. References crun_index on the routes end point.
schedule_index int(11) FALSE FALSE 1 Foreign Key Constraint. References the schedule end point.

route_tickets Reference table linking routes and contract_runs

Resource Data Type Read Only Null Default Description
Return To Top
rt_index int(11)(AI) TRUE PRIMARY KEY index used for updating via API
crun_index int(11) FALSE FALSE 1 Foreign Key Constraint. References crun_index on the contract_runs endpoint.
route_index int(11) FALSE FALSE 1 Foreign Key Constraint. References route_index on the routes endpoint.

schedule Scheduling codes.

Note: the Scheduling class included in our project on github makes calls to this endpoint unnecessary.

Resource Data Type Read Only Null Default Description
Return To Top
schedule_index int(11) TRUE PRIMARY KEY index used for referencing schedule codes and literals.
code varchar(3) TRUE 2 or 3 character scheduling code. [a-g][1-9] or h5 - h28
literal varchar(25) TRUE Literal schedule. Ex: Every Thursday or Every Last Weekday or Every 17th

dispatchers Information relating to dispatchers.

Resource Data Type Read Only Null Default Description
Return To Top
dispatch_index int(11)(AI) TRUE PRIMARY KEY index used for updating via API
DispatchID int(11) FALSE NO None This values should be confirmed unique before submission.
FirstName varchar(20) FALSE NO Default Name
LastName varchar(20) FALSE YES NULL
EmailAddress text FALSE YES NULL
Password varchar(255) FALSE NO None Hash only. No raw passwords should be stored in the database
LoggedIn tinyint(1) FALSE NO 0 bool
LastSeen datetime FALSE YES NULL
Deleted tinyint(1) FALSE NO 0 bool

driversInformation relating to drivers.

Resource Data Type Read Only Null Default Description
Return To Top
driver_index int(11)(AI) TRUE PRIMARY KEY index used for updating via API
DriverID int(11) FALSE NO None This value should be confirmed unique before submission.
FirstName varchar(20) FALSE NO Default Name
LastName varchar(20) FALSE YES NULL
EmailAddress varchar(254) FALSE YES NULL
Password varchar(255) FALSE NO None Hash only. No raw passwords should stored in the database
LoggedIn tinyint(1) FALSE NO 0 bool
LastSeen datetime FALSE YES NULL
CanDispatch int(1) FALSE NO 0 Describes to whom the driver can dispatch: 0 = None, 1 = Self, 2 = Any
Deleted tinyint(1) FALSE NO 0 bool

clientsInformation relating to repeat clients.

Resource Data Type Read Only Null Default Description
Return To Top
client_index int(11)(AI) TRUE PRIMARY KEY index used for updating via API
RepeatClient tinyint(1) FALSE NO 1 bool: 0 = Non-Repeat Client, 1 = Repeat Client
ClientID int(11) FALSE NO None This value should be confirmed unique before submission.
ClientName varchar(45) FALSE NO Default Name
Department varchar(45) FALSE YES NULL
ShippingAddress1 varchar(45) FALSE NO None
ShippingAddress2 varchar(45) FALSE NO None
ShippingCountry varchar(2) FALSE NO None Country abbreviation generated with this function
BillingName varchar(45) FALSE Yes NULL
BillingAddress1 varchar(45) FALSE Yes NULL
BillingAddress2 varchar(45) FALSE Yes NULL
BillingCountry varchar(2) FALSE Yes NULL Country abbreviation generated with this function
Same tinyint(1) FALSE No 0 bool. Indicates if shipping and billing name and address are the same.
Telephone varchar(20) FALSE YES NULL
EmailAddress varchar(254) FALSE YES NULL
Attention varchar(45) FALSE YES NULL
ContractDiscount decimal(5,2) FALSE NO 0.00
GeneralDiscount decimal(5,2) FALSE NO 0.00
StandardVAT decimal(4,2) FALSE NO 0.00 Standard rate for VAT on tickets.
ReducedVAT decimal(4,2) FALSE NO 0.00 Reduced rate for VAT on tickets.
VATtype int(1) FALSE NO 0 0 = Not VAT-able, 1 = Client 0 Standard, 2 = Client 0 Reduced, 3 = This Client Standard, 4 = This Client Reduced, 5 = Zero-Rated, 6 = Exempt
VATtypeIce int(1) FALSE NO 0 0 = Not VAT-able, 1 = Client 0 Standard, 2 = Client 0 Reduced, 3 = This Client Standard, 4 = This Client Reduced, 5 = Zero-Rated, 6 = Exempt
DIPO tinyint(1) FALSE NO 0 bool. Indicates that the client has a dry ice price different from config.
DIPrice decimal(5,2) FALSE NO 0 Alternate dry ice price for client.
ClientTerms int(1) FALSE NO 0 Indicates terms for each client. 0 = Default, 1 = Due Upon Receipt, 2 = Net D, 3 = Net EOM D, 4 = A/B Net D.
DiscountRate decimal(4,2) FALSE NO 0.00 The percentage discount (A) offered for A/B Net D terms.
DiscountWindow int(3) FALSE NO 0 The number of days (B) a discount rate is offered for A/B Net D terms.
TermLength int(3) FALSE NO 0 The period of days (D) that terms are extended.
EmailInvoice tinyint(1) FALSE NO 0 bool: Shoud invoices be delivered vai email or other method.
Organization int(11) FALSE NO 0 References the ID field of the o_clients end point
org_id int(11) FLASE NO 1 Foreign Key Constraint. References o_client_index on the o_clients endpoint. This value is returned when an organization is succfully entered via the API, an array of values is returned if organizations are batch entered.
Deleted tinyint(1) FALSE NO 0 bool
Password varchar(255) FALSE NO Hash of default password Hash only. No raw passwords should be stored in the database
AdminPassword varchar(255) FALSE NO Hash of default password Hash only. No raw passwords should be stored in the database

o_clientsInformation on clients grouped together as an organization.

Resource Data Type Read Only Null Default Description
Return To Top
o_client_index int(11)(AI) TRUE PRIMARY KEY index used for updating via API
id int(11) FALSE NO None This value should be confirmed unique before submission.
Name varchar(40) FALSE NO Default Name
Login varchar(40) FALSE NO default
Password varchar(255) FALSE NO Hash of default password Hash only. No raw passwords should be stored in the database
ListBy int(1) FALSE NO 0 0 = Street Address, 1 = Department
RequestTickets int(1) FALSE NO 0 0 = No ticket requesting. 1 = Members can bill to other members. 2 = Org can request tickets. >=3 = 1 + 2
Deleted tinyint(1) FALSE NO 0 bool

schedule_overrideInformation relating to exceptions to contract/repeating run scheduling.

Resource Data Type Read Only Null Default Description
Return To Top
s_o_index int(11)(AI) TRUE PRIMARY KEY index used for updating via API
crun_index int(11) FALSE NO 1 Foreign Key Constraint. References crun_index on the contract_runs endpoint. This value is returned when a run is succfully entered via the API, an array of values is returned if runs are batch entered. The Cancel All Runs event uses crun_index 1.
route_index int(11) FALSE NO 1 Foreign Key Constraint. References route_index on the routes endpoint. This value is returned when a route is succfully entered via the API, an array of values is returned if runs are batch entered. Cancelations use route_index 1.
Cancel tinyint(1) FALSE NO 1 bool: 0 = reschedule run at crun_index and put it on route at route_index, 1 = cancel run at crun_index.
StartDate date FALSE NO None
EndDate date FALSE NO None
pTime time FALSE NO 00:00:00 New Pick Up time if Cancel = 0
dTime time FALSE NO 00:00:00 New Drop Off time if Cancel = 0
d2Time time FALSE YES NULL New Return Time if Cancel = 0
Deleted tinyint(1) FALSE NO 0 bool

ticketsInformation relating to tickets.

Resource Data Type Read Only Null Default Description
Return To Top
ticket_index int(11)(AI) TRUE PRIMARY KEY index used for updating via API
TicketNumber bigint(20) FALSE NO 0 This value should be confirmed unique before submission.
RunNumber int(11) FALSE NO 0 References contract_runs
BillTo int(11) FALSE NO 0 References clients.
RepeatClient tinyint(1) FALSE NO 1 bool: Refers to the value of the RepeatClient field in the clients table.
RequestedBy varchar(45) FALSE YES NULL
pClient varchar(45) FALSE NO Pick Up Client Pick up location name
pDepartment varchar(45) FALSE YES NULL Pick up location department
pContact varchar(45) FALSE YES NULL Pick up location contact
pTelephone varchar(20) FALSE YES NULL Pick up location contact telephone
pAddress1 varchar(45) FALSE NO - Pick up building number, street, room number
pAddress2 varchar(45) FALSE NO - Picket up city/town, state/province, zip/post code
pCountry varchar(2) FALSE NO - Pick up country abbreviation generated with this function
dClient varchar(45) FALSE NO Delivery Client Delivery location name
dDepartment varchar(45) FALSE YES NULL Delivery location department
dContact varchar(45) FALSE YES NULL Delivery location contact
dTelephone varchar(20) FALSE YES NULL Delivery location contact telephone
dAddress1 varchar(45) FALSE NO - Delivery building number, street, room number
dAddress2 varchar(45) FALSE NO - Delivery city/town, state/province, zip/post code
dCountry varchar(2) FALSE NO - Delivery country abbreviation generated with this function
dryIce tinyint(1) FALSE NO 0 bool: 0 = No Dry Ice, 1 = Dry Ice
diWeight decimal(6,3) FALSE NO 0.00 Total weight of dry ice included with the delivery
diPrice decimal(6,2) FALSE NO 0.00 Total charge for dry ice included with the delivery
TicketBase decimal(6,2) FALSE NO 0.00 Unmodified ticket price
Charge int(1) FALSE NO 5 0 = Canceled, 1 = 1 hour, 2 = 2 hour, 3 = 3 hour, 4 = 4 hour, 5 = Routine, 6 = Round Trip, 7 = Dedicated Run, 8 = Dead Run, 9 = Credit
Contract tinyint(1) FALSE NO 0 bool: 0 = On Call, 1 = Contract
Multiplier float FALSE NO 1
RunPrice decimal(6,2) FALSE NO 0.00 Ticket price after modification by Charge, before modification by VATrate or Multiplier.
VATable tinyint(1) FALSE NO 0 bool. Is this ticket subject to VAT.
VATrate decimal(4,2) FALSE NO 0.00 The percentage rate of applicable VAT.
VATtype int(1) FALSE NO 0 0 = Not VAT-able, 1 = Standard, 2 = Reduced, 3 = Client Standard, 4 = Client Reduced, 5 = Zero-Rated, 6 = Exempt
VATableIce tinyint(1) FALSE NO 0 bool. Is the dry ice on this ticket subject to VAT.
VATrateIce decimal(4,2) FALSE NO 0.00 The percentage rate of applicable VAT.
VATtypeIce int(1) FALSE NO 0 0 = Not VAT-able, 1 = Standard, 2 = Reduced, 3 = Client Standard, 4 = Client Reduced, 5 = Zero-Rated, 6 = Exempt
TicketPrice decimal(6,2) FALSE NO 0.00 Final ticket price after Charge modification, dry ice addition, Multiplier, and applicable VAT.
Notes text FALSE YES NULL
Telephone varchar(20) FALSE YES NULL Telephone number of billed client
EmailConfirm int(1) FALSE NO 0 0 = none, 1 = On Pick Up, 2 = On Delivery, 3 = On Pick Up and On Delivery, 4 = On Return, 5 = On Pick Up and On Return, 6 = On Delivery and On Return, 7 = At Each Step
EmailAddress varchar(255) FALSE YES NULL
pSigReq tinyint(1) FALSE NO 0 Request signature on pick up.
pSigPrint varchar(45) FALSE YES NULL
pSig mediumblob FALSE YES NULL For storing captured image of signature. png or jpg format. Maximum size of (2^24) - 1 bytes (16MB)
pSigType varchar(4) FALSE YES NULL Identifies the file extension of binary data stored in pSig without a dot.
See image_type_to_extension
dSigReq tinyint(1) FALSE NO 0 Request signature on delivery.
dSigPrint varchar(45) FALSE YES NULL
dSig mediumblob FALSE YES NULL For storing captured image of signature. png or jpg format. Maximum size of (2^24) - 1 bytes (16MB)
dSigType varchar(4) FALSE YES NULL Identifies the file extension of binary data stored in dSig without a dot.
See image_type_to_extension
d2SigReq tinyint(1) FALSE NO 0 Request signature on return.
d2SigPrint varchar(45) FALSE YES NULL
d2Sig mediumblob FALSE YES NULL For storing captured image of signature. png or jpg format. Maximum size of (2^24) - 1 bytes (16MB)
d2SigType varchar(4) FALSE YES NULL Identifies the file extension of binary data stored in d2Sig without a dot.
See image_type_to_extension
pSigFileName varchar(256) FALSE YES NULL Name of pick up signature file.
dSigFileName varchar(256) FALSE YES NULL Name of delivery signature file.
d2SigFileName varchar(256) FALSE YES NULL Name of return signature file.
NotForDispatch tinyint(1) FALSE NO 0 If set to 1 the API should not return this ticket to drivers or dispatchers.
DispatchTimeStamp datetime FALSE YES NULL
DispatchMicroTime varchar(7) FALSE NO .0 String representation of the mircotime created by parsing microtime(). Ex: .123456
ReadyDate datetime FALSE YES NULL If a delivery is not ready for pick up when a request is made this value can be used to indicate this to a driver.
DispatchedTo int(11) FALSE NO 0 References drivers
DispatchedBy varchar(7) FALSE NO 1.1

Coded representation of who dispatched a ticket, ex: 1.2. Left of the dot refers to the level of the user 1 = dispatcher, 2 = driver. Right of the dot refers to the dispatcher / driver ID.

ReceivedDate datetime FALSE NO None
Transfers longtext FALSE YES NULL json encoded string representing ticket transfers. This should be a numeric array containing objects. Child objects should have only the following properties:

"holder": (number) ID of driver who had the ticket originally,

"receiver": (number) ID of driver receiving the ticket,

"transferedBy": (string) ID of driver / dispatcher transferring the ticket (see DispatchedBy),

"timestamp": (number) Unix timestamp.

Ex: [ { "holder":1, "receiver":2, "transferedBy":"2.1", "timestamp":1512574403296 }, { "holder":2, "receiver":1, "transferedBy":"2.2", "timestamp":1512574797926 } ]
TransferState tinyint(1) FALSE NO 0 Describes the current state of a transfer. 0 = inactive, 1 = pending
PendingReceiver int(11) FALSE NO 0 Driver ID of the target receiver of the current transfer.
pTimeStamp datetime FALSE YES NULL
dTimeStamp datetime FALSE YES NULL
d2TimeStamp datetime FALSE YES NULL
pLat decimal(8,6) FALSE YES NULL Pick up location latitude
pLng decimal(9,6) FALSE YES NULL Pick up location longitude
dLat decimal(8,6) FALSE YES NULL Delivery location latitude
dLng decimal(9,6) FALSE YES NULL Delivery location longitude
d2Lat decimal(8,6) FALSE YES NULL Return location latitude
d2Lng decimal(9,6) FALSE YES NULL Return location longitude
pTime time FALSE YES NULL Used for scheduling
dTime time FALSE YES NULL Used for scheduling
d2Time time FALSE YES NULL Used for scheduling
invoice_id int(11) FALSE NO 1 Foreign Key Constraint. References invoice_index on the invoices endpoint. This value is returned when an invoice is succfully entered via the API, an array of values is returned if invoices are batch entered.
InvoiceNumber varchar(15) FALSE NO - Indicates what invoice, if any, the ticket is billed on.
Expects an invoice number in one of two formats:
1: ##EX####-## regex: /(^[\d]{2}EX[\d]+-[\d]+$)/
2: ##EX####-t## regex: /(^[\d]{2}EX[\d]+-t[\d]+$)/

invoicesInformation relating to invoices.

Resource Data Type Read Only Null Default Description
Return To Top
invoice_index int(11)(AI) TRUE PRIMARY KEY index used for updating via API.
InvoiceNumber varchar(15) FALSE NO 00EX0000-0 This value should be confirmed unique before submission.
Expects an invoice number in one of two formats:
1: ##EX####-## regex: /(^[\d]{2}EX[\d]+-[\d]+$)/
2: ##EX####-t## regex: /(^[\d]{2}EX[\d]+-t[\d]+$)/
The presence of a 't' after the dash indicates RepeatClient value of 0.
InvoiceTerms int(1) FALSE NO 1 Indicates the terms an invoice. 1 = Due Upon Receipt, 2 = Net D, 3 = Net EOM D, 4 = A/B Net D.
DiscountRate decimal(4,2) FALSE NO 0.00 The percentage discount (A) offered for A/B Net D terms.
DiscountWindow int(3) FALSE NO 0 The number of days (B) a discount rate is offered for A/B Net D terms.
TermLength int(3) FALSE NO 0 The period of days (D) that terms are extended.
ClientID int(11) FALSE NO 0
RepeatClient tinyint(1) FALSE NO 1 Reference RepeatClient field of the clients table.
BalanceForwarded decimal(6,2) FALSE NO 0.00 This value is taken from the Balance field of the most recently closed invoice.
InvoiceSubTotal decimal(6,2) FALSE NO 0.00 This value is the sum of all tickets included on the invoice.
AmountDue decimal(6,2) FALSE NO 0.00 This value is calculated when an invoice is closed and is the difference between InvoiceSubTotal and BalanceForwarded.
InvoiceTotal decimal(6,2) FALSE NO 0.00 This value is the sum of InvoiceSubTotal, BalanceForwarded, and any past due invoices.
StartDate date FALSE NO None
EndDate date FALSE NO None
DateIssued date FALSE NO None
DatePaid date FALSE YES NULL
AmountPaid decimal(6,2) FALSE NO 0.00
Balance decimal(6,2) FALSE NO 0.00 This value is calculated when an invoice is closed and is the difference between AmountDue and AmountPaid.
Late30Invoice varchar(16) FALSE YES NULL Identifies Invoice(s) that are 30 days past due at the time of current invoice creation.
Expects an invoice number in one of four formats:
1: ##EX####-## regex: /(^[\d]{2}EX[\d]+-[\d]+$)/
2: ##EX####-t## regex: /(^[\d]{2}EX[\d]+-t[\d]+$)/
3: ##EX####-##+ regex: /(^[\d]{2}EX[\d]+-[\d]+\+$)/
4: ##EX####-t##+ regex: /(^[\d]{2}EX[\d]+-t[\d]+\+$)/
Late30Value decimal(8,2) FALSE YES NULL Sum of InvoiceSubTotal of Invoice(s) that are 30 days past due at the time of current invoice creation
Late60Invoice varchar(16) FALSE YES NULL Identifies Invoice(s) that are 60 days past due at the time of current invoice creation.
Expects an invoice number in one of four formats:
1: ##EX####-## regex: /(^[\d]{2}EX[\d]+-[\d]+$)/
2: ##EX####-t## regex: /(^[\d]{2}EX[\d]+-t[\d]+$)/
3: ##EX####-##+ regex: /(^[\d]{2}EX[\d]+-[\d]+\+$)/
4: ##EX####-t##+ regex: /(^[\d]{2}EX[\d]+-t[\d]+\+$)/
Late60Value decimal(8,2) FALSE YES NULL Sum of InvoiceSubTotal of Invoice(s) that are 60 days past due at the time of current invoice creation
Late90Invoice varchar(16) FALSE YES NULL Identifies Invoice(s) that are 90 days past due at the time of current invoice creation.
Expects an invoice number in one of four formats:
1: ##EX####-## regex: /(^[\d]{2}EX[\d]+-[\d]+$)/
2: ##EX####-t## regex: /(^[\d]{2}EX[\d]+-t[\d]+$)/
3: ##EX####-##+ regex: /(^[\d]{2}EX[\d]+-[\d]+\+$)/
4: ##EX####-t##+ regex: /(^[\d]{2}EX[\d]+-t[\d]+\+$)/
Late90Value decimal(8,2) FALSE YES NULL Sum of InvoiceSubTotal of Invoice(s) that are 90 days past due at the time of current invoice creation.
Over90Invoice varchar(50) FALSE YES NULL Identifies Invoice(s) that are more than 90 days past due at the time of current invoice creation.
Expects either a comma separated list of up to 4 Invoice Numbers as described on the InvoiceNumber end point or a comma separated list of three such Invoice Numbers followed by a plus symbol (+) an integer and the word 'more'. The last case should look like this (the letter 't' should follow the dash (-) in the case of non repeat clients):
##EX####-##, ##EX####-##, ##EX####-##, + ### more. Note the comma between the last invoice number and the plus symbol (+), if this is omitted data validation will fail.
Over90Value decimal(8,2) FALSE YES NULL Sum of InvoiceSubTotal of Invoice(s) that are more than 90 days past due at the time of current invoice creation
PaymentLink varchar(256) FALSE YES NULL Link for online payment.
CheckNumber varchar(50) FALSE YES NULL Check number, transaction number, or any other identifier for payment method.
Closed tinyint(1) FALSE NO 0 bool
Deleted tinyint(1) FALSE NO 0 bool

webhooksInformation relating to webhook listeners.

Resource Data Type Read Only Null Default Description
Note: Currently the POST method is disabled for this endpoint.
Return To Top
webhook_index int(11)AI TRUE NO None PRIMARY KEY index used for updateing via API.
Protocol tinyint(1) FALSE NO 1 bool: 0 = http, 1 = https
Listener varchar(255) FALSE NO None URL listening for webhooks.
Events text FALSE NO None Comma-separated list of dot-notated events. Ex: ticket.oncall.receive,ticket.oncall.dispatch (Note: There are no white-spaces in this string). See Webhooks page for full list of values.
Deleted tinyint(1) FALSE NO 0 Boolean indicating if the webhook has been deactivated.
<?php
  function countryFromAbbr($abbr) {
    // Credits will have a value of '-' for pCountry and dCountry
    if ($abbr === '-') return $abbr;
    //Country names and abbreviations taken from FedEx international shipping guidelines
    //"XZ" is not on the abbreviation list so it will stand for "Not On File"
    if (strlen($abbr) === 2) {
      switch ($abbr) {
        case 'AL': return 'Albania';
        case 'DZ': return 'Algeria';
        case 'AD': return 'Andorra';
        case 'AO': return 'Angola';
        case 'AR': return 'Argentina';
        case 'AM': return 'Armenia';
        case 'AW': return 'Aruba';
        case 'AU': return 'Australia';
        case 'AT': return 'Austria';
        case 'AZ': return 'Azerbaijan';
        case 'PT': return 'Azores';
        case 'BS': return 'Bahamas';
        case 'BH': return 'Bahrain';
        case 'BD': return 'Bangladesh';
        case 'BB': return 'Barbados';
        case 'BY': return 'Belarus';
        case 'BE': return 'Belgium';
        case 'BZ': return 'Belize';
        case 'BJ': return 'Benin';
        case 'BM': return 'Bermuda';
        case 'BT': return 'Bhutan';
        case 'BO': return 'Bolivia';
        case 'BA': return 'Bosna-Herzegovina';
        case 'BW': return 'Botswana';
        case 'BR': return 'Brazil';
        case 'BN': return 'Brunei Darussalam';
        case 'BG': return 'Bulgaria';
        case 'BF': return 'Burkina FASO';
        case 'BI': return 'Burundi';
        case 'KH': return 'Cambodia';
        case 'CM': return 'Cameroon';
        case 'CA': return 'Canada';
        case 'CV': return 'Cape Verde';
        case 'KY': return 'Cayman Islands';
        case 'CF': return 'Cntl African Republic';
        case 'TS': return 'Chad';
        case 'CL': return 'Chile';
        case 'CN': return 'China';
        case 'CO': return 'Columbia';
        case 'ZR': return 'Democratic Republic of Congo';
        case 'CG': return 'Republic of the Congo (Brazaville)';
        case 'CR': return  'Costa Rica';
        case 'CI': return 'Cote d\'Ivoire (Ivory Coast)';
        case 'HR': return 'Croatia';
        case 'CY': return 'Cyprus';
        case 'CZ': return 'Czech Republic';
        case 'DK': return 'Denmark';
        case 'DJ': return 'Djibouti';
        case 'DO': return 'Dominican Republic';
        case 'EC': return 'Ecuador';
        case 'EG': return 'Egypt';
        case 'SV': return 'El Salvador';
        case 'GQ': return 'Equatorial Guinea';
        case 'ER': return 'Eritrea';
        case 'EE': return 'Estonia';
        case 'ET': return 'Ethiopia';
        case 'DK': return 'Faroe Islands';
        case 'FJ': return 'Fiji';
        case 'FI': return 'Finland';
        case 'FR': return 'France';
        case 'GF': return 'French Guiana';
        case 'PF': return 'French Polynesia (Tahitti)';
        case 'GA': return 'Gabon';
        case 'GE': return 'Georgia, Republic of';
        case 'DE': return 'Germany';
        case 'GH': return 'Ghana';
        case 'GB': return 'Great Britain & Northern Ireland';
        case 'GR': return 'Greece';
        case 'GD': return 'Grenada';
        case 'GP': return 'Guadeloupe';
        case 'GT': return 'Guatemala';
        case 'GN': return 'Guinea';
        case 'GW': return 'Guinea-Bissau';
        case 'GY': return 'Guyana';
        case 'HT': return 'Haiti';
        case 'HN': return 'Honduras';
        case 'HK': return 'Hong Kong';
        case 'HU': return 'Hungary';
        case 'IS': return 'Iceland';
        case 'IN': return 'India';
        case 'ID': return 'Indonesia';
        case 'IR': return 'Iran';
        case 'IQ': return 'Iraq';
        case 'IE': return 'Ireland (Eire)';
        case 'IL': return 'Israel';
        case 'IT': return 'Italy';
        case 'JM': return 'Jamaica';
        case 'JP': return 'Japan';
        case 'JO': return 'Jordan';
        case 'KG': return 'Kazakhstan';
        case 'KE': return 'Kenya';
        case 'KR': return 'South Korea, Republic of';
        case 'KW': return 'Kuwait';
        case 'KG': return 'Kyrgyzstan';
        case 'LA': return 'Laos';
        case 'LV': return 'Latvia';
        case 'LS': return 'Lesotho';
        case 'LR': return 'Liberia';
        case 'LI': return 'Liechtenstein';
        case 'LT': return 'Lithuania';
        case 'LU': return 'Luxembourg';
        case 'MO': return 'Macao';
        case 'MK': return 'Macedonia, Republic of';
        case 'MG': return 'Madagascar';
        case 'PT': return 'Madeira Islands';
        case 'MW': return 'Malawi';
        case 'MY': return 'Malaysia';
        case 'MV': return 'Maldives';
        case 'ML': return 'Mali';
        case 'MT': return 'Malta';
        case 'MQ': return 'Martinique';
        case 'MR': return 'Mauritania';
        case 'MU': return 'Mauritius';
        case 'MX': return 'Mexico';
        case 'MD': return 'Moldova';
        case 'MN': return 'Mongolia';
        case 'MA': return 'Morocco';
        case 'MZ': return 'Mozambique';
        case 'NA': return 'Namibia';
        case 'NR': return 'Nauru';
        case 'NP': return 'Nepal';
        case 'NL': return 'Netherlands (Holland)';
        case 'AN': return 'Netherlands Antilles';
        case 'NC': return 'New Caledonia';
        case 'NZ': return 'New Zealand';
        case 'NI': return 'Nicaragua';
        case 'NE': return 'Niger';
        case 'NG': return 'Nigeria';
        case 'NO': return 'Norway';
        case 'OM': return 'Oman';
        case 'PK': return 'Pakistan';
        case 'PA': return 'Panama';
        case 'PG': return 'Papua New Guinea';
        case 'PY': return 'Paraguay';
        case 'PE': return 'Peru';
        case 'PH': return 'Philippines';
        case 'PL': return 'Poland';
        case 'PT': return 'Portugal';
        case 'QA': return 'Qatar';
        case 'RO': return 'Romania';
        case 'RU': return 'Russia (Russia Federation)';
        case 'RW': return 'Rwanda';
        case 'KN': return 'St. Christopher (St. Kitts) & Nevis';
        case 'LC': return 'St. Lucia';
        case 'VC': return 'St. Vincent & the Grenadines';
        case 'SA': return 'Saudi Arabia';
        case 'SN': return 'Senegal';
        case 'YU': return 'Serbia Montenegro (Yugoslavia)';
        case 'SC': return 'Seychelles';
        case 'SL': return 'Sierra Leone';
        case 'SG': return 'Singapore';
        case 'SK': return 'Slovak Republic (Slovakia)';
        case 'SI': return 'Slovenia';
        case 'SB': return 'Solomon Islands';
        case 'SO': return 'Somalia';
        case 'ZA': return 'South Africa';
        case 'ES': return 'Spain';
        case 'LK': return 'Sri Lanka';
        case 'SD': return 'Sudan';
        case 'SZ': return 'Swaziland';
        case 'SE': return 'Sweden';
        case 'CH': return 'Switzerland';
        case 'SY': return 'Syrian Arab Republic';
        case 'TW': return 'Taiwan';
        case 'TJ': return 'Tajikistan';
        case 'TZ': return 'Tanzania';
        case 'TH': return 'Thailand';
        case 'TG': return 'Togo';
        case 'TT': return 'Trinidad & Tobago';
        case 'TN': return 'Tunisia';
        case 'TR': return 'Turkey';
        case 'TM': return 'Turkmenistan';
        case 'UG': return 'Uganda';
        case 'AE': return 'United Arab Emirates';
        case 'UA': return 'Ukraine';
        case 'US': return 'United States of America';
        case 'UY': return 'Uruguay';
        case 'VU': return 'Vanuatu';
        case 'VE': return 'Venezuela';
        case 'VN': return 'Vietnam';
        case 'WS': return 'Western Samoa';
        case 'YE': return 'Yemen';
        default: return 'Not On File';
      }
    } else {
      switch ($abbr) {
        case 'Albania': return 'AL';
        case 'Algeria': return 'DZ';
        case 'Andorra': return 'AD';
        case 'Angola': return 'AO';
        case 'Argentina': return 'AR';
        case 'Armenia': return 'AM';
        case 'Aruba': return 'AW';
        case 'Australia': return 'AU';
        case 'Austria': return 'AT';
        case 'Azerbaijan': return 'AZ';
        case 'Azores': return 'PT';
        case 'Bahamas': return 'BS';
        case 'Bahrain': return 'BH';
        case 'Bangladesh': return 'BD';
        case 'Barbados': return 'BB';
        case 'Belarus': return 'BY';
        case 'Belgium': return 'BE';
        case 'Belize': return 'BZ';
        case 'Benin': return 'BJ';
        case 'Bermuda': return 'BM';
        case 'Bhutan': return 'BT';
        case 'Bolivia': return 'BO';
        case 'Bosna-Herzegovina': return 'BA';
        case 'Botswana': return 'BW';
        case 'Brazil': return 'BR';
        case 'Brunei Darussalam': return 'BN';
        case 'Bulgaria': return 'BG';
        case 'Burkina FASO': return 'BF';
        case 'Burundi': return 'BI';
        case 'Cambodia': return 'KH';
        case 'Cameroon': return 'CM';
        case 'Canada': return 'CA';
        case 'Cape Verde': return 'CV';
        case 'Cayman Islands': return 'KY';
        case 'Cntl African Republic': return 'CF';
        case 'Chad': return 'TS';
        case 'Chile': return 'CL';
        case 'China': return 'CN';
        case 'Columbia': return 'CO';
        case 'Democratic Republic of Congo': return 'ZR';
        case 'Republic of the Congo (Brazaville)': return 'CG';
        case 'Costa Rica': return 'CR';
        case 'Cote d\'Ivoire (Ivory Coast)': return 'CI';
        case 'Croatia': return 'HR';
        case 'Cyprus': return 'CY';
        case 'Czech Republic': return 'CZ';
        case 'Denmark': return 'DK';
        case 'Djibouti': return 'DJ';
        case 'Dominican Republic': return 'DO';
        case 'Ecuador': return 'EC';
        case 'Egypt': return 'EG';
        case 'El Salvador': return 'SV';
        case 'Equatorial Guinea': return 'GQ';
        case 'Eritrea': return 'ER';
        case 'Estonia': return 'EE';
        case 'Ethiopia': return 'ET';
        case 'Faroe Islands': return 'DK';
        case 'Fiji': return 'FJ';
        case 'Finland': return 'FI';
        case 'France': return 'FR';
        case 'French Guiana': return 'GF';
        case 'French Polynesia (Tahitti)': return 'PF';
        case 'Gabon': return 'GA';
        case 'Georgia, Republic of': return 'GE';
        case 'Germany': return 'DE';
        case 'Ghana': return 'GH';
        case 'Great Britain & Northern Ireland': return 'GB';
        case 'Greece': return 'GR';
        case 'Grenada': return 'GD';
        case 'Guadeloupe': return 'GP';
        case 'Guatemala': return 'GT';
        case 'Guinea': return 'GN';
        case 'Guinea-Bissau': return 'GW';
        case 'Guyana': return 'GY';
        case 'Haiti': return 'HT';
        case 'Honduras': return 'HN';
        case 'Hong Kong': return 'HK';
        case 'Hungary': return 'HU';
        case 'Iceland': return 'IS';
        case 'India': return 'IN';
        case 'Indonesia': return 'ID';
        case 'Iran': return 'IR';
        case 'Iraq': return 'IQ';
        case 'Ireland (Eire)': return 'IE';
        case 'Israel': return 'IL';
        case 'Italy': return 'IT';
        case 'Jamaica': return 'JM';
        case 'Japan': return 'JP';
        case 'Jordan': return 'JO';
        case 'Kazakhstan': return 'KG';
        case 'Kenya': return 'KE';
        case 'South Korea, Republic of': return 'KR';
        case 'Kuwait': return 'KW';
        case 'Kyrgyzstan': return 'KG';
        case 'Laos': return 'LA';
        case 'Latvia': return 'LV';
        case 'Lesotho': return 'LS';
        case 'Liberia': return 'LR';
        case 'Liechtenstein': return 'LI';
        case 'Lithuania': return 'LT';
        case 'Luxembourg': return 'LU';
        case 'Macao': return 'MO';
        case 'Macedonia, Republic of': return 'MK';
        case 'Madagascar': return 'MG';
        case 'Madeira Islands': return 'PT';
        case 'Malawi': return 'MW';
        case 'Malaysia': return 'MY';
        case 'Maldives': return 'MV';
        case 'Mali': return 'ML';
        case 'Malta': return 'MT';
        case 'Martinique': return 'MQ';
        case 'Mauritania': return 'MR';
        case 'Mauritius': return 'MU';
        case 'Mexico': return 'MX';
        case 'Moldova': return 'MD';
        case 'Mongolia': return 'MN';
        case 'Morocco': return 'MA';
        case 'Mozambique': return 'MZ';
        case 'Namibia': return 'NA';
        case 'Nauru': return 'NR';
        case 'Nepal': return 'NP';
        case 'Netherlands (Holland)': return 'NL';
        case 'Netherlands Antilles': return 'AN';
        case 'New Caledonia': return 'NC';
        case 'New Zealand': return 'NZ';
        case 'Nicaragua': return 'NI';
        case 'Niger': return 'NE';
        case 'Nigeria': return 'NG';
        case 'Norway': return 'NO';
        case 'Oman': return 'OM';
        case 'Pakistan': return 'PK';
        case 'Panama': return 'PA';
        case 'Papua New Guinea': return 'PG';
        case 'Paraguay': return 'PY';
        case 'Peru': return 'PE';
        case 'Philippines': return 'PH';
        case 'Poland': return 'PL';
        case 'Portugal': return 'PT';
        case 'Qatar': return 'QA';
        case 'Romania': return 'RO';
        case 'Russia (Russia Federation)': return 'RU';
        case 'Rwanda': return 'RW';
        case 'St. Christopher (St. Kitts) & Nevis': return 'KN';
        case 'St. Lucia': return 'LC';
        case 'St. Vincent & the Grenadines': return 'VC';
        case 'Saudi Arabia': return 'SA';
        case 'Senegal': return 'SN';
        case 'Serbia Montenegro (Yugoslavia)': return 'YU';
        case 'Seychelles': return 'SC';
        case 'Sierra Leone': return 'SL';
        case 'Singapore': return 'SG';
        case 'Slovak Republic (Slovakia)': return 'SK';
        case 'Slovenia': return 'SI';
        case 'Solomon Islands': return 'SB';
        case 'Somalia': return 'SO';
        case 'South Africa': return 'ZA';
        case 'Spain': return 'ES';
        case 'Sri Lanka': return 'LK';
        case 'Sudan': return 'SD';
        case 'Swaziland': return 'SZ';
        case 'Sweden': return 'SE';
        case 'Switzerland': return 'CH';
        case 'Syrian Arab Republic': return 'SY';
        case 'Taiwan': return 'TW';
        case 'Tajikistan': return 'TJ';
        case 'Tanzania': return 'TZ';
        case 'Thailand': return 'TH';
        case 'Togo': return 'TG';
        case 'Trinidad & Tobago': return 'TT';
        case 'Tunisia': return 'TN';
        case 'Turkey': return 'TR';
        case 'Turkmenistan': return 'TM';
        case 'Uganda': return 'UG';
        case 'United Arab Emirates': return 'AE';
        case 'Ukraine': return 'UA';
        case 'United States of America': return 'US';
        case 'Uruguay': return 'UY';
        case 'Vanuatu': return 'VU';
        case 'Venezuela': return 'VE';
        case 'Vietnam': return 'VN';
        case 'Western Samoa': return 'WS';
        case 'Yemen': return 'YE';
        default: return 'XZ';
      }
    }
  }
      
Return To Top
<?php
  function call($method, $url, $payload=false) {
    $privateKey = 'Your_Private_API_Key';
    // Get the time
    $time = time();
    // Use api key to generate security token using the REQUEST_URI and the time
    $token = hash_hmac('sha256', substr($url, strpos($url, '.com') + 4) . $time, $privateKey);
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_FAILONERROR, true);
    // CURLOPT_SSL_VERIFYPEER set to false for testing only
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    // CURLOPT_SSL_VERIFYHOST disabled for testing only. It should be set to 2 in production
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
    curl_setopt($ch, CURLOPT_CAINFO, '/path/to/cacrt.pem');
    //Set the authorization headers
    $headers = [];
    $hearers[] = 'Authorization: Basic ' . base64_encode("Your_Account_Number:Your_Public_API_Key");
    $headers[] = "X-CI-Auth: $token";
    $headers[] = "X-CI-Time: $time";
    if ($payload) {
      // $payload should be an array of Resource Names and Values.
      $payloadJSON = json_encode($payload);
      curl_setopt($ch, CURLOPT_POSTFIELDS, $payloadJSON);
      $headers[] = 'Content-Type: application/json';
      $headers[] = 'Content-Length: ' . strlen($payloadJSON);
    }
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    $result = curl_exec($ch);
    if ($result === false) {
      return curl_error($ch);
    }
    return $result;
  }
      
Return To Top
<?php
  function buildURI($endPoint, $data) {
    /***
    * $data['include'] = [ 'res1', 'res2', 'res3', ... ];
    * Indicates specific resources to fetch from the endpoint
    * If $data['include'] is omitted all resources for entries matching the filter will be returned
    *
    * $data['exclude'] = [ 'res1', 'res2', 'res3', ... ];
    * Indicates specific resources not to fetch from the endpoint
    *
    * If both 'include' and 'exclude' are present 'include' will be favored
    *
    * $data['filter'] = [ [ 'Resource'=>'resName', 'Filter'=>'filter', 'Value'=>'val' ] ];
    * The above describes a simple 'AND' query. 
    * The structure of an 'AND' statement simply addds elements to the primary array. EX:
    * $data['filter'] = [
    *   [ 'Resource'=>'resName', 'Filter'=>'filter', 'Value'=>'val' ],
    *   [ 'Resource'=>'resName', 'Filter'=>'filter', 'Value'=>'val' ]
    * ];
    * An 'OR' statement requires an array of simple 'AND' statements. Ex:
    * $data['filter'] = [
    *   [ 
    *     [ 'Resource'=>'resName', 'Filter'=>'filter', 'Value'=>'val' ],
    *     [ 'Resource'=>'resName', 'Filter'=>'filter', 'Value'=>'val' ]
    *   ],
    *   [ 'Resource'=>'resName', 'Filter'=>'filter', 'Value'=>'val' ]
    * ]
    * The available filters:
    *   cs: contain string (string contains value)
    *   sw: start with (string starts with value)
    *   ew: end with (string end with value)
    *   eq: equal (string or number matches exactly)
    *   lt: lower than (number is lower than value)
    *   le: lower or equal (number is lower than or equal to value)
    *   ge: greater or equal (number is higher than or equal to value)
    *   gt: greater than (number is higher than value)
    *   bt: between (number is between two comma separated values)
    *   in: in (number is in comma separated list of values)
    *   is: is null (field contains "NULL" value)
    *   Negate any filter by prepending a 'n' character, ex: 'eq' becomes 'neq'
    *
    *  If $data['filter'] is omitted requested resources will be returned for all entries
    *
    *  With the "order" parameter you can sort by resource.
    *  The default sort is in ascending order, but by specifying "desc" this can be reversed
    *  $data['order'] = array(array('resource'=>'TicketNumber','dir'=>'desc'));
    *
    *  You may sort on multiple fields by using multiple "order" parameters
    *  $data['order'] = [ [ 'resource'=>'TicketNumber', 'dir'=>'desc' ], [ 'resource'=>'Contract' ] ];
    *
    *
    * The "page" parameter holds the requested page. The default page size is 20, but can be adjusted (e.g. to 50)
    * $data['page'] = '1'; or $data['page'] = '1,50';
    *
    * Requests that are not ordered cannot be paginated
    *
    * The "join" parameter is an indexed array of tables that use Foreign Key Constraints. Those constraints are described
    * in the above tables. Ex:
    *
    * $endpoint = 'invoices';
    * $data['filter'][] = ['Resource'=>'BillTo', 'Filter'=>'eq', 'Value'=>0];
    * $data['join'] = ['tickets'];
    *
    * This would return all invoices for client ID 0 and each of those invoices would have the resource 'Tickets'
    * which is an indexed array containing each of the tickets associated with that invoice.
    *
    *  When using this function to update a resource (PUT):
    *    $data should be an empty array
    *    This functions return should have a string appended
    *    This string should begin with a forward slash followed by the PRIMARY KEY value of the resource to be modified
    *    An example using the call function defined above:
    *      call('PUT', buildURI($endPoint, []) . '/' . $primaryKey, $payload);
    *    $payload should be an array of key => value pairs to be updated at the $endPoint
    *    See the tables above for the PRIMARY KEY resources to be used for these calls
    ***/
    $baseURI = "https://rjdeliveryomaha.com/v2/records/";
    $queryURI = $baseURI . $endPoint;
    if (empty($data)) {
      return $queryURI;
    }
    $query = [];
    // make sure the keys in $data are in the proper order
    $paramList = [];
    $params = $data;
    if (isset($data['exclude'])) {
      $paramList[] = 'exclude';
    }
    if (isset($data['include'])) {
      $paramList[] = 'include';
    }
    if (isset($data['filter'])) {
      $paramList[] = 'filter';
    }
    if (isset($data['order'])) {
      $paramList[] = 'order';
    }
    if (isset($data['page'])) {
      $paramList[] = 'page';
    }
    if (isset($data['join'])) {
      $paramList[] = 'join';
    }
    $data = array_merge(array_flip($paramList), $params);
    
    foreach ($data as $key => $value) {
      if ($key === 'exclude' && (!isset($data['include']) || empty($data['include']))) {
        $query['exclude'] = implode(',', $data['exclude']);
      }
      if ($key === 'include') {
        $query['include'] = implode(',', $data['include']);
      } elseif ($key === 'filter') {
        if (!isset($value[0][0])) {
          for ($i = 0; $i < count($value); $i++) {
            $this->query['filter'][] = implode(',', array_values($value[$i]));
          }
        } else {
          for ($i = 0; $i < count($value); $i++) {
            $filter_index = $i + 1;
            for ($j = 0; $j < count($value[$i]); $j++) {
              $this->query["filter{$filter_index}"][] = implode(',', array_values($value[$i][$j]));
            }
          }
        }
      }
    }
    if (!empty($query)) {
      $encodedQuery = http_build_query($query, null, '&', PHP_QUERY_RFC3986);
      // http://php.net/manual/en/function.http-build-query.php#111819
      $encodedQuery = preg_replace('/%5B[0-9]+%5D/simU', '', $encodedQuery);
    } else {
      $encodedQuery = '';
    }
    if (isset($data['order'])) {
      for ($i = 0; $i < count($data['order']); $i++) {
        if (strlen($encodedQuery) > 0) $encodedQuery .= '&';
        $orderParam = preg_replace('/\s+/', '', $data['order'][$i]);
        $encodedQuery .= "order={$orderParam}";
      }
      if (isset($data['page'])) {
        $paramTest = false;
        if (strpos(',', $data['page']) !== false) {
          $testVal = explode(',', $data['page']);
          $paramTest = count($testVal) === 2 && is_numeric($testVal[0]) && is_numeric($testVal[1]);
        } else {
          $paramTest = is_numeric($data['page']);
        }
        if ($paramTest === true) $encodedQuery .= "&page={$data['page']}";
      }
    }
    if (isset($data['join'])) {
      for ($i = 0; $i < count($data['join']); $i++) {
        if (strlen($encodedQuery) > 0) $encodedQuery .= '&';
        $joinParam = preg_replace('/\s+/', '', $this->queryParams['join'][$i]);
        $encodedQuery .= "join={$joinParam}";
      }
    }
    return "{$queryURI}?{$encodedQuery}";
  }
      
Return To Top
<?php
  function testResponse($method, $response) {
    // A simple test to make sure an appropriate response was received
    switch ($method) {
      // POST: Expects the id of the last created resource
      // PUT, DELETE: Expects the number of rows affected
      // Expects an array of ids or rows affected when creating, updating, or deleting with array
      // Receives the string 'null' on failure
      case "DELETE":
      case "PUT":
      case "POST": return (is_numeric($response) || substr($response, 0, 1) === '[');
      // Expects a json encoded object or array of objects
      case "GET": return (substr($response,0,1) === '{' || substr($response,0,1) === '[');

      default: return false;
    }
  }
      
Return To Top
<?php
  function responseError($response) {
    switch (filter_var($response, FILTER_SANITIZE_NUMBER_INT)) {
      case 400: echo 'Server Error: Invalid Request URI.'; break;
      case 401: echo 'Server Error: Invalid login credentials.'; break;
      case 403: echo 'Server Error: Login credentials not defined.'; break;
      case 404: echo 'Server Error: Failed to locate record.'; break;
      case 422: echo 'Server Error: Failed Data Validation. ' . substr($response, strpos($response,'422')+3); break;
      case 500: echo 'Server Error: Internal Error.'; break;
      case 503: echo "Server Error: Service temporarily unavailable.\n"; default:
      default: echo 'Server Error: ' . $response; break;
    }
  }
      
Return To Top
<?php
  /**
  usage:
    if ($_SERVER['REQUEST_METHOD'] !== "POST") return false;
    http_response_code(200); // PHP 5.4 or greater
    include_once("../ PATH / To / webhookHandler.class.php");
    $webhookHandler = new webhookHandler(@file_get_contents("php://input"));
    $webhookHandler->processWebhook();
  **/
  class webhookHandler {
    // user config
    private $timezone = "UTC";
    private $privateKey = "Your_Private_API_Key";
    private $userID = "Your_Account_Number";
    // properties in the webhook
    private $error;
    private $type;
    private $BillTo;
    private $DispatchedTo;
    private $TicketNumber;
    private $InvoiceNumber;
    private $ClientID;
    private $DriverID;
    private $DispatchID;
    private $RunNumber;
    private $ScheduleID;
    private $stored;
    private $received;
    // validation
    private $token;
    private $inputArray;
    // processing
    private $typeParts;
    private $transferKeys = [
      "error", "type", "BillTo", "DispatchedTo", "TicketNumber", "InvoiceNumber", "ClientID", "DriverID",
      "DispatchID", "stored", "received"
    ];
    // error handling
    private $content;
    private $fileWriteTry = 5;
    private $targetFile = "./logs/webhookHandler_error";
    private $Data;
    
    function __construct($jsonString) {
      if ($jsonString === NULL || $jsonString === false) $this->exitWithoutLog();
      
      $this->inputArray = json_decode($jsonString);
      
      if (json_last_error() !== JSON_ERROR_NONE || $this->inputArray === NULL) $this->exitWithoutLog();
      
      $this->token = hash_hmac('sha256', $jsonString . $this->userID . $this->inputArray->timestamp, $this->privateKey);
      // hash_equals for php < 5.6.0
      // https://php.net/manual/en/function.hash-equals.php#115664
      // random_bytes alternatives for php < 7.0
      // https://www.php.net/manual/en/function.random-bytes.php#118932
      $headers = array_change_key_case(getallheaders(), CASE_UPPER);
      $auth = $headers['X-CI-AUTH'] ?? bin2hex(random_bytes(1e4));
      if (!hash_equals($this->token, $auth)) {
        $this->content = "Failed Auth \n" . print_r($jsonString, true);
        $this->exitWithLog();
      }
      
      if (!isset($this->inputArray["Data"]) || empty($this->inputArray["Data"])) {
        $this->content = "No webhook to process \n"  . print_r($jsonString, true);
      } else {
        $this->Data = $this->inputArray["Data"];
      }
      
      if (!date_default_timezone_set($this->timezone)) {
        $this->content = "Timezone error: Could not set timezone to \"{$this->timezone}\n----\n\n";
        $this->writeLoop();
      }
    }
    
    private function exitWithoutLog() {
      return false;
    }
    
    private function exitWithLog() {
      $this->content = ($this->content === NULL) ?
        date("dMY H:i:s") . "\nundefined error\n----\n\n" :
        date("dMY H:i:s") . "\n" . $this->content . "\n----\n\n";
        
      return $this->writeLoop();
    }

    private function writeLoop() {
      $i = 0;
      do {
        $test = $this->writeFile();
        $i++;
      } while ($test !== strlen($this->content) && $i < $this->fileWriteTry);
    }

    private function writeFile() {
      /*** http://php.net/manual/en/function.fwrite.php#81269 ***/
      $fp = fopen( $this->targetFile, "ab" );
      /*** write the new file content ***/
      $bytes_to_write = strlen($this->content);
      $bytes_written = 0;
      while ($bytes_written < $bytes_to_write) {
        if ($bytes_written == 0) {
          $rv = fwrite($fp, $this->content);
        } else {
          $rv = fwrite($fp, substr($this->content, $bytes_written));
        }
        if ($rv === false || $rv == 0) {
          return($bytes_written == 0 ? false : $bytes_written);
        }
        $bytes_written += $rv;
      }
      return $bytes_written;
    }
    
    public function processWebhook() {
      for ($i = 0; $i < count($this->Data); $i++) {
        // TODO
        // Implement deduplication for batch webhooks if desired
        foreach ($this->Data[$i] as $key => $value) {
          foreach ($this as $k => $v) {
            if (in_array($key, $this->transferKeys) && $key === $k) {
              $this->$k = $value;
            }
          }
        }
        $this->executeWebhook();
      }
    }
    
    private function executeWebhook() {
      if (empty($this->Data)) $this->exitWithoutLog();
      if ($this->error !== NULL) {
        $this->content = $this->error;
        $this->exitWithLog();
      } else {
        $this->typeParts = explode(".", $this->type);
        $method = $this->typeParts[0];
        if (method_exists($this, $method)) return $this->$method();
        $this->content = "Invalid primary hook component: {$this->type}";
        $this->exitWithLog();
      }
    }
    // ticket methods
    private function ticket() {
      $method = $this->typeParts[2] . "Ticket";
      switch($this->typeParts[1]) {
        case "contract":
          // TODO
          // differentiate between contract and on call tickets
        case "oncall":
          if (method_exists($this, $method)) return $this->$method();
          $this->content = "Invalid tertiary hook component: {$this->type}";
          $this->exitWithLog();
        break;
        default:
          $this->content = "Invalid secondary hook component: {$this->type}";
          $this->exitWithLog();
      }
    }
    
    private function receiveTicket() {
      $this->content = "Ticket {$this->TicketNumber} received";
      $this->content .= "\n" . print_r($this->Data, true);
      $this->exitWithLog();
    }
    
    private function dispatchTicket() {
      $this->content = "Ticket {$this->TicketNumber} dispatched to driver {$this->DispatchedTo}";
      $this->content .= "\n" . print_r($this->Data, true);
      $this->exitWithLog();
    }
    
    private function pickupTicket() {
      $this->content = "Ticket {$this->TicketNumber} picked up";
      $this->content .= "\n" . print_r($this->Data, true);
      $this->exitWithLog();
    }
    
    private function deliverTicket() {
      $this->content = "Ticket {$this->TicketNumber} delivered";
      $this->content .= "\n" . print_r($this->Data, true);
      $this->exitWithLog();
    }
    
    private function returnTicket() {
      $this->content = "Ticket {$this->TicketNumber} returned";
      $this->content .= "\n" . print_r($this->Data, true);
      $this->exitWithLog();
    }
    
    private function updateTicket() {
      if (
        count(get_object_vars($this->received)) === 2 &&
        property_exists($this->received, "InvoiceNumber") & &
        property_exists($this->received, "invoice_id")
      ) {
        // uncomment the following to prevent sending push notifications
        // for each ticket when invoices are created via the API
        // return false;
      }
      if (
        count(get_object_vars($this->received)) === 2 &&
        ((property_exists($this->received, 'pLat') &&
        property_exists($this->received, 'pLng')) ||
        (property_exists($this->received, 'dLat') &&
        property_exists($this->received, 'dLng')) ||
        (property_exists($this->received, 'd2Lat') &&
        property_exists($this->received, 'd2Lng')))
      ) {
        // uncomment the following to prevent sending push notifications when coordinates are updated for only one step
        // return false;
      }
      $this->content = "Ticket {$this->TicketNumber} updated";
      $this->content .= "\n" . print_r($this->Data, true);
      $this->exitWithLog();
    }
    
    private function transferTicket() {
      $this->content = "Ticket {$this->TicketNumber} transferred";
      $this->content .= "\n" . print_r($this->Data, true);
      $this->exitWithLog();
    }
    
    private function deleteTicket() {
      $this->content = "Ticket {$this->TicketNumber} deleted";
      $this->content .= "\n" . print_r($this->Data, true);
      $this->exitWithLog();
    }
    // invoice methods
    private function invoice() {
      $method = $this->typeParts[1] . "Invoice";
      if(method_exists($this, $method)) return $this->$method();
      $this->content = "Invalid secondary hook component: {$this->type}";
      $this->exitWithLog();
    }
    
    private function createInvoice() {
      $this->content = "Invoice {$this->InvoiceNumber} created";
      $this->content .= "\n" . print_r($this->Data, true);
      $this->exitWithLog();
    }
    
    private function updateInvoice() {
      $this->content = "Invoice {$this->InvoiceNumber} updated";
      $this->content .= "\n" . print_r($this->Data, true);
      $this->exitWithLog();
    }
    
    private function deleteInvoice() {
      $this->content = "Invoice {$this->InvoiceNumber} deleted";
      $this->content .= "\n" . print_r($this->Data, true);
      $this->exitWithLog();
    }
    // client methods
    private function client() {
      $method = $this->typeParts[2] . "Client";
      if(method_exists($this, $method)) return $this->$method();
      $this->content = "Invalid secondary hook component: {$this->type}";
      $this->exitWithLog();
    }
    
    private function createClient() {
      $this->content = "{$this->typeParts[1]} Client {$this->ClientID} {$this->typeParts[2]}d";
      $this->content .= "\n" . print_r($this->Data, true);
      $this->exitWithLog();
    }
    
    private function updateClient() {
      return $this->createClient();
    }
    
    private function deleteClient() {
      return $this->createClient();
    }
    
    private function t_client() {
      return $this->client();
    }
    
    private function o_client() {
      return $this->client();
    }
    // driver / dispatcher methods
    private function driver() {
      $method = $this->typeParts[1] . "Driver";
      if(method_exists($this, $method)) return $this->$method();
      $this->content = "Invalid secondary hook component: {$this->type}";
      $this->exitWithLog();
    }
    
    private function createDriver() {
      $this->content = "{$this->type[0]} {$this->DriverID} {$this->typeParts[1]}d";
      $this->content .= "\n" . print_r($this->Data, true);
      $this->exitWithLog();
    }
    
    private function updateDriver() {
      return $this->createDriver();
    }
    
    private function deleteDriver() {
      return $this->createDriver();
    }
    
    private function dispatcher() {
      $method = $this->typeParts[1] . "Dispatcher";
      if(method_exists($this, $method)) return $this->$method();
      $this->content = "Invalid secondary hook component: {$this->type}";
      $this->exitWithLog();
    }
    
    private function createDispatcher() {
      $this->content = "{$this->type[0]} {$this->DispatchID} {$this->typeParts[1]}d";
      $this->content .= "\n" . print_r($this->Data, true);
      $this->exitWithLog();
    }
    
    private function updateDispatcher() {
      return $this->createDispatcher();
    }
    
    private function deleteDispatcher() {
      return $this->createDispatcher();
    }
    
    private function contract_run() {
      $method = $this->typeParts[1] . "Run";
      if(method_exists($this, $method)) return $this->$method();
      $this->content = "Invalid secondary hook component: {$this->type}";
      $this->exitWithLog();
    }
    
    private function createRun() {
      $this->content = "{$this->type[0]} {$this->RunNumber} {$this->typeParts[1]}d";
      $this->content .= "\n" . print_r($this->Data, true);
      $this->exitWithLog();
    }
    
    private function updateRun() {
      return $this->createRun();
    }
    
    private function deleteRun() {
      return $this->createRun();
    }
    
    private function schedule_override() {
      $method = $this->typeParts[1] . "Schedule";
      if(method_exists($this, $method)) return $this->$method();
      $this->content = "Invalid secondary hook component: {$this->type}";
      $this->exitWithLog();
    }
    
    private function createSchedule() {
      $this->content = "{$this->type[0]} {$this->ScheduleID} {$this->typeParts[1]}d";
      $this->content .= "\n" . print_r($this->Data, true);
      $this->exitWithLog();
    }
    
    private function updateSchedule() {
      return $this->createSchedule();
    }
    
    private function deleteSchedule() {
      return $this->createSchedule();
    }
  }
      
Return To Top
<?php
  /**
    this should be run as a cron job the day following the end of a monthly billing cycle.
    In terms of timing this is very simplistic this class does not account for leap years and assumes that invoices
    will not be generated after the 28th of any month
    
    usage:
    include_once '../ PATH / TO /invoiceCron.class.php';
    $invoiceCron = new invoiceCron();
    $invoiceCron->createInvoices();
  **/
  class invoiceCron {
    private $username = 'Your_Account_Number';
    private $publicKey = 'Your_Public_API_Key';
    private $privateKey = 'Your_Private_API_Key';
    private $timezoneString = 'UTC';
    private $logSuccess = false;
    // array of (int)ClientID that should not be processed on this schedule
    private $ignoreClients = [];
    private $ignoreNonRepeat = [];
    // invoice variables
    private $defaultTerms = 1;
    private $discountRate = 0;
    private $discountWindow = 0;
    private $termLength = 0;
    private $startDate;
    private $endDate;
    private $dateIssued;
    private $tickets;
    private $clientList;
    private $newInvoices;
    private $DateIssued;
    private $Over90InvoiceList;
    // update variables
    private $ticketUpdateKeys;
    private $ticketUpdateValues;
    // query variables
    private $method;
    private $primaryKey;
    private $baseURI;
    private $ch;
    private $timeVal;
    private $token;
    private $queryURI;
    private $headers;
    private $jsonData;
    private $message;
    private $query;
    private $result;
    private $payload = false;
    private $data;
    private $endPoint;
    // error catching
    private $timezone;
    private $today;
    private $content;
    private $fileWriteTry = 5;
    private $targetFile = './invoice_error';
    
    public function __construct() {
      // Extend timeout for large queries
      set_time_limit(3600);
      $this->clients =
      $this->t_clients =
      $this->clientList =
      $this->headers =
      $this->query =
      $this->data =
      $this->ticketUpdateKeys =
      $this->ticketUpdateValues = [];
      // set timezone
      try {
        $this->timezone = new dateTimezone($this->timezoneString);
      } catch (Exception $e) {
        $this->content = date('Y-m-d H:i:s') . "\nTimezone Error: {$e->getMessage()}\n\n";
        $this->writeLoop();
        exit;
      }
      // set today's date
      try {
        $this->today = new dateTime('NOW', $this->timezone);
      } catch(Exception $e) {
        $this->content = date('Y-m-d H:i:s') . "\nDate Error: {$e->getMessage()}\n\n";
        $this->writeLoop();
        exit;
      }
      // set the invoice start date
      $tempDate = clone $this->today;
      $tempDate->modify('- 1 month');
      $this->startDate = $tempDate->format('Y-m-d');
      // set the invoice end date
      $tempDate = clone $this->today;
      $tempDate->modify('- 1 day');
      $this->endDate = $tempDate->format('Y-m-d');
      // set DateIssued for all invoices
      $this->DateIssued = $this->today->format('Y-m-d');
      // define the base uri
      $this->baseURI = 'https://rjdeliveryomaha/v2/records/';
    }
    
    public function createInvoices() {
      // fetch all tickets that have not been billed this cycle
      $this->fetchTickets();
      // fetch the terms for clients with tickets to be invoiced
      $this->fetchTerms();
      // fetch most recent invoice numbers and forwarded balances
      $this->fetchLastInvoice();
      // process new invoices
      $this->processInvoices();
      // submit invoices to the API
      $this->submitInvoices();
      // submit ticket updates to API
      $this->submitTickets();
      // log success
      if ($this->logSuccess === true) {
        $this->content =
          $this->today->format("d M Y H:i:s.u") . "\n" .
          count($this->newInvoices) . " Invoices Created\n\n";
        $this->writeLoop();
      }
      exit;
    }
    // start utility functions
    private function clearParameters() {
      $this->headers = $this->query = $this->data = [];
      $this->jsonData = '';
      $this->payload = false;
    }

    private function test_int($val) {
      return (int)round($this->test_float($val), 0, PHP_ROUND_HALF_EVEN);
    }

    private function test_float($val) {
      return (float)filter_var($val, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION);
    }
    // Search function for returning part of a string
    private function after($needle, $haystack) {
      if (!is_bool(strpos($haystack, $needle)))
      return substr($haystack, strpos($haystack,$needle)+strlen($needle));
    }

    private function before($needle, $haystack) {
      return substr($haystack, 0, strpos($haystack, $needle));
    }

    private function between($needle, $needle2, $haystack) {
      return $this->before($needle2, $this->after($needle, $haystack));
    }
    
    private function writeLoop() {
      $i = 0;
      do {
        $test = $this->writeFile();
        $i++;
      } while ($test !== strlen($this->content) && $i < $this->fileWriteTry);
    }

    private function writeFile() {
      /*** http://php.net/manual/en/function.fwrite.php#81269 ***/
      /*** open the file for writing and truncate it to zero length ***/
      $fp = fopen( $this->targetFile, 'ab' );
      /*** write the new file content ***/
      $bytes_to_write = strlen($this->content);
      $bytes_written = 0;
      while ($bytes_written < $bytes_to_write) {
        if ($bytes_written == 0) {
          $rv = fwrite($fp, $this->content);
        } else {
          $rv = fwrite($fp, substr($this->content, $bytes_written));
        }
        if ($rv === false || $rv == 0) {
          return($bytes_written == 0 ? false : $bytes_written);
        }
        $bytes_written += $rv;
      }
      return $bytes_written;
    }

    private function testResponse() {
      // A simple test to make sure an appropriate response was received
      switch (strtoupper($this->method)) {
        // POST: Expects the id of the last created resource
        // PUT, DELETE: Expects the number of rows affected
        // Expects an array of ids or rows affected when creating, updating, or deleting with array
        // Receives the string 'null' on failure
        case 'DELETE':
        case 'PUT':
        case 'POST': return (is_numeric($this->result) || substr($this->result, 0, 1) === '[');
        // Expects a json encoded object or array of objects
        case 'GET': return (substr($this->result,0,1) === '{' || substr($this->result,0,1) === '[');
    
        default: return false;
      }
    }

    private function responseError() {
      switch ($this->test_int($this->result)) {
        case 400:
          $this->content = "Server Error: Invalid Request URI.\n";
        break;
        case 401:
          $this->content = "Server Error: Invalid login credentials.\n";
        break;
        case 403:
          $this->content = "Server Error: Login credentials not defined.\n";
        break;
        case 404:
          $this->content = "Server Error: Failed to locate record.\n";
        break;
        case 422:
          $this->content = "Server Error: Failed Data Validation. {$this->after('422', $this->result)}\n";
        break;
        case 500:
          $this->content = "Server Error: Internal Error.\n";
        break;
        case 503:
          $this->content = "Server Error: Service temporarily unavailable.\n";
        default:
          $this->content = "Server Error: {$this->result}.\n";
        break;
      }
      $this->writeLoop();
      exit;
    }

    private function buildURI() {
      if (!is_array($this->data) || empty($this->data)) {
        $this->queryURI = $this->baseURI . $this->endPoint;
      } else {
        foreach ($this->data as $key => $value) {
          if ($key === 'resources') {
            $this->query['include'] = implode(",",$this->data['resources']);
          } elseif ($key === 'filter') {
            if (!isset($value[0][0])) {
              for ($i = 0; $i < count($value); $i++) {
                $this->query['filter'][] = implode(',', array_values($value[$i]));
              }
            } else {
              for ($i = 0; $i < count($value); $i++) {
                $filter_index = $i + 1;
                for ($j = 0; $j < count($value[$i]); $j++) {
                  $this->query["filter{$filter_index}"][] = implode(',', array_values($value[$i][$j]));
                }
              }
            }
          } else {
            $this->query[$key] = implode(',',$value);
          }
        }
        $temp2 = http_build_query($this->query,NULL,'&',PHP_QUERY_RFC3986);
        // http://php.net/manual/en/function.http-build-query.php#111819
        $temp2 = preg_replace('/%5B[0-9]+%5D/simU', '', $temp2);
        $this->queryURI = "{$this->baseURI}{$this->endPoint}?{$temp2}";
      }
    }

    private function call() {
      if ($this->primaryKey != NULL) {
        $this->queryURI .= "/{$this->primaryKey}";
      }
      // Use api key to generate security token
      $this->timeVal = time();
      // Generate the security key using the REQUEST_URI
      $this->token =
        hash_hmac(
          'sha256',
          substr($this->queryURI,
          strpos($this->queryURI, '.com') + 4) . $this->timeVal,
          $this->privateKey
        );
      $this->ch = curl_init();
      curl_setopt($this->ch, CURLOPT_CUSTOMREQUEST, strtoupper($this->method));
      curl_setopt($this->ch, CURLOPT_URL, $this->queryURI);
      curl_setopt($this->ch, CURLOPT_FAILONERROR, TRUE);
      // CURLOPT_SSL_VERIFYPEER set to false for testing only
      curl_setopt($this->ch, CURLOPT_SSL_VERIFYPEER, TRUE);
      // CURLOPT_SSL_VERIFYHOST set to 0 for testing only
      curl_setopt($this->ch, CURLOPT_SSL_VERIFYHOST, 2);
      curl_setopt($this->ch, CURLOPT_CAINFO, __DIR__ . DIRECTORY_SEPARATOR . 'cacert.pem');
      // Set the authorization headers
      $this->headers[] = "Authorization: Basic " . base64_encode("{$this->username}:{$this->publicKey}");
      $this->headers[] = "X-CI-Auth: {$this->token}";
      $this->headers[] = "X-CI-Time: {$this->timeVal}";
      if ($this->payload !== false) {
        $this->jsonData = json_encode($this->payload);
        curl_setopt($this->ch, CURLOPT_POSTFIELDS, $this->jsonData);
        $this->headers[] = 'Content-Type: application/json';
        $this->headers[] = 'Content-Length: ' . strlen($this->jsonData);
      }
      curl_setopt($this->ch, CURLOPT_HTTPHEADER, $this->headers);
      curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, TRUE);
      $this->result =  curl_exec($this->ch);
      if ($this->result === false) {
        $this->result = curl_error($this->ch);
      }
      curl_close($this->ch);
      if (!$this->testResponse()) {
        $this->responseError();
      }
    }
    // end utility functions
    private function fetchTickets() {
      $this->clearParameters();
      $this->endPoint = 'tickets';
      $this->method = 'GET';
      $this->data['resources'] = [ 'ticket_index', 'TicketPrice', 'Charge', 'RepeatClient', 'BillTo' ];
      // Create filter for repeat clients
      $repeatFilter = [
        [ 
          'Resource'=>'ReceivedDate',
          'Filter'=>'bt',
          'Value'=>"{$this->startDate} 00:00:00, {$this->endDate} 11:59:59"
        ],
        [ 'Resource'=>'InvoiceNumber', 'Filter'=>'eq', 'Value'=>'-' ],
        [ 'Resource'=>'RepeatClient', 'Filter'=>'eq', 'Value'=>1 ]
      ];
      if (!empty($this->ignoreClients)) $repeatFilter[] =
        [ 'Resource'=>'ClientID', 'Filter'=>'nin', 'Value'=>implode(',', $this->ignoreClients) ];
      // Create filter for non-repeat clients
      $nonrepeatFilter = [
        [
          'Resource'=>'ReceivedDate',
          'Filter'=>'bt',
          'Value'=>"{$this->startDate} 00:00:00, {$this->endDate} 11:59:59"
        ],
        [ 'Resource'=>'InvoiceNumber', 'Filter'=>'eq', 'Value'=>'-' ],
        [ 'Resource'=>'RepeatClient', 'Filter'=>'eq', 'Value'=>0 ]
      ];
      if (!empty($this->ignoreNonRepeat)) $repeatFilter[] =
        [ 'Resource'=>'ClientID', 'Filter'=>'nin', 'Value'=>implode(',', $this->ignoreNonRepeat) ];
        
      $this->data['filter'] = [ $repeatFilter, $nonrepeatFilter ];
      $this->buildURI();
      $this->call();
      $temp = json_decode($this->result);
      if (empty($temp->records)) {
        $this->content =
          "{$this->today->format('d M Y H:i:s.u')}\nNo Tickets To Process\n\n" .
          print_r($this->data, true) . "\n" . print_r($this->result, true) . "\n----\n";
        $this->writeLoop();
        exit;
      }
      for ($i = 0; $i < count($temp->records); $i++) {
        $key = ($temp->records[$i]['RepeatClient'] == 1) ?
          $temp->records[$i]['BillTo'] : "t{$temp->records[$i]['BillTo']}";
          
        if (!array_key_exists($key, $this->clientList)) $this->clientList[$key] =
          [
            'tickets'=>[],
            'lastInvoice'=>[],
            'openInvoices'=>[],
            'terms'=> [
              'InvoiceTerms'=>1,
              'DiscountRate'=>0,
              'DiscountWindow'=>0,
              'TermLength'=>0
            ]
          ];
          
        $this->clientList[$key]['tickets'][] = $temp[$i];
      }
    }

    private function fetchTerms() {
      $this->clearParameters();
      $this->endPoint = 'clients';
      $this->method = 'GET';
      $this->data['resources'] = [
        'ClientID', 'RepeatClient', 'ClientTerms', 'DiscountRate', 'DiscountWindow', 'TermLength'
      ];
      // Split repeat and non-repeat clientIDs into separate arrays
      $repeats = $nonrepeats = $repeatFilter = $nonrepeatFilter = [];
      foreach($this->clientList as $key => $value) {
        if (strpos($key,'t') === false) {
          $repeats[] = $key;
        } else {
          $nonrepeats[] = substr($key, 1);
        }
      }
      if (!empty($repeats)) {
        $repeatFilter = [
          ['Resource'=>'ClientID', 'Filter'=>'in', 'Value'=>implode(',', $repeats)],
          ['Resource'=>'RepeatClient', 'Filter'=>'eq', 'Value'=>1]
        ];
      }
      if (!empty($nonrepeats)) {
        $nonrepeatFilter = [
          ['Resource'=>'ClientID', 'Filter'=>'in', 'Value'=>implode(',', $nonrepeats)],
          ['Resource'=>'RepeatClient', 'Filter'=>'eq', 'Value'=>0]
        ];
      }
      if (!empty($repeatFilter) && !empty($nonrepeatFilter)) {
        $this->data['filter'] = [ $repeatFilter, $nonrepeatFilter ];
      } else {
        $this->data['filter'] = (empty($nonrepeatFilter)) ? $repeatFilter : $nonrepeatFilter;
      }
      $this->buildURI();
      $this->call();
      for ($i = 0; $i < count($this->result); $i++) {
        $key = $this->result[$i]['RepeatClient'] == 1 ?
          $this->result[$i]['ClientID'] : "t{$this->result[$i]['ClientID']}";
        if ($this->result[$i]['ClientTerms'] == 0) {
          $this->clientList[$key]['terms']['InvoiceTerms'] = $this->defaultTerms;
          $this->clientList[$key]['terms']['DiscountRate'] = $this->discountRate;
          $this->clientList[$key]['terms']['DiscountWindow'] = $this->discountWindow;
          $this->clientList[$key]['terms']['TermLength'] = $this->termLength;
        } else {
          $this->clientList[$key]['terms']['InvoiceTerms'] = $this->result[$i]['ClientTerms'];
          $this->clientList[$key]['terms']['DiscountRate'] = $this->result[$i]['DiscountRate'];
          $this->clientList[$key]['terms']['DiscountWindow'] = $this->result[$i]['DiscountWindow'];
          $this->clientList[$key]['terms']['TermLength'] = $this->result[$i]['TermLength'];
        }
      }
    }
    
    private function fetchLastInvoice() {
      $this->clearParameters();
      $this->endPoint = 'invoices';
      $this->method = 'GET';
      $this->data['resources'] =
        [ 'InvoiceNumber', 'RepeatClient', 'BalanceForwarded', 'InvoiceSubTotal', 'DateIssued', 'Closed', 'Deleted' ];
      
      $repeats = $nonrepeats = $repeatFilter = $nonrepeatFilter = [];
      foreach($this->clientList as $key => $value) {
        if (strpos($key,'t') === false) {
          $repeats[] = $key;
        } else {
          $nonrepeats[] = substr($key, 1);
        }
      }
      if (!empty($repeats)) {
        $repeatFilter = [ 
          [ 'Resource'=>'ClientID', 'Filter'=>'in', 'Value'=>implode(',', $repeats) ],
          [ 'Resource'=>'RepeatClient', 'Filter'=>'eq', 'Value'=>1 ]
        ];
      }
      if (!empty($nonrepeats)) {
        $nonrepeatFilter = [
          [ 'Resource'=>'ClientID', 'Filter'=>'in', 'Value'=>implode(',', $nonrepeats) ],
          [ 'Resource'=>'RepeatClient', 'Filter'=>'eq', 'Value'=>0 ]
        ];
      }
      if (!empty($repeatFilter) && !empty($nonrepeatFilter)) {
        $this->data['filter'] = [ $repeatFilter, $nonrepeatFilter ];
      } else {
        $this->data['filter'] = (empty($nonrepeatFilter)) ? $repeatFilter : $nonrepeatFilter;
      }
      $this->buildURI();
      $this->call();
      $temp = json_decode($this->result);
      for ($i = 0; $i < count($temp->records); $i++) {
        $tempID =
          substr($temp->records[$i]['InvoiceNumber'], strpos($temp->records[$i]['InvoiceNumber'],'-') + 1);
        if ($temp->records[$i]['Closed'] === 0 && $temp->records[$i]['Deleted'] === 0) {
            $this->clientList[$tempID]['openInvoices'][] = $temp->records[$i];
        }
        if (empty($this->clientList[$tempID]['lastInvoice'])) {
          $this->clientList[$tempID]['lastInvoice'] = $temp->records[$i];
        } else {
          $this->clientList[$tempID]['lastInvoice'] =
            ($this->clientList[$tempID]['lastInvoice']['DateIssued'] > $temp->records[$i]['DateIssued']) ?
            $this->clientList[$tempID]['lastInvoice'] : $temp->records[$i];
        }
      }
    }
    
    private function processInvoices() {
      foreach ($this->clientList as $key => $value) {
        if (empty($value['tickets'])) continue;
        // create invoice object for submission
        $tempInvoice = new stdClass();
        $tempInvoice->ClientID = $this->test_int($key);
        $tempInvoice->RepeatClient = (substr($key,0,1) === 't') ? 0 : 1;
        $tempInvoice->StartDate = $this->startDate;
        $tempInvoice->EndDate = $this->endDate;
        $tempInvoice->DateIssued = $this->DateIssued;
        // create new InvoiceNumber
        $invoicePointer = mt_rand(1000, 1100);
        if (!empty($value['lastInvoice'])) {
          $invoicePointer = (int)$this->between('X', '-', $value['lastInvoice']['InvoiceNumber']) + 1;
        }
        $tempInvoice->InvoiceNumber = "{$this->today->format('y')}EX{$invoicePointer}-{$key}";
        // solve the invoice subtotal and prep tickets to be update with new invoice number
        $tempInvoice->InvoiceTotal =
          $tempInvoice->InvoiceSubTotal = $this->getTotal($value['tickets'], $tempInvoice->InvoiceNumber);
        // solve the amount due for the current invoice
        $tempInvoice->AmountDue =
          (!empty($value['lastInvoice'])) ?
          $tempInvoice->InvoiceSubTotal - $value['lastInvoice']['BalanceForwarded'] :
          $tempInvoice->InvoiceSubTotal;
        // solve the total due for all open invoices
        if (array_key_exists('openInvoices', $value) && !empty($value['openInvoices'])) {
          // Past due invoices need to be sortted by age and added to InvoiceTotal
          $date1 = $this->today;
          for ($i = 0; $i < count($value['openInvoices']); $i++) {
            $flag30 = $flag60 = $flag90 = $flagover90 = false;
            try {
              $date2 = new dateTime($value['openInvoices'][$i]['DateIssued'], $this->timezone);
            } catch(Exception $e) {
              $this->content =
                "{$today->format("d M Y H:i:s.u")}\nDate Error Line " . __line__ . ": {$e->getMessage()}\n\n";
              $this->writeLoop();
              exit;
            }
            $diff = $date2->diff($date1);
            if ($value['openInvoices'][$i]['InvoiceTerms'] == 3) {
              switch ($diff->m) {
                case 0:
                  $flag30 = $date1->format('n') != $date2->format('n') &&
                    $date1->format('j') >= $value['openInvoices'][$i]['TermLength'];
                  break;
                case 1:
                  $flag30 = true;
                  break;
                case 2:
                  $flag60 = true;
                  break;
                case 3:
                  $flag90 = true;
                  break;
                default: $flagover90 = true;
              }
            } else {
              if (
                ($value['openInvoices'][$i]['InvoiceTerms'] == 1 && $diff->days < 60) ||
                ($value['openInvoices'][$i]['InvoiceTerms'] > 1 &&
                ($value['openInvoices'][$i]['TermLength'] <= $diff->days &&
                  $diff->days < $value['openInvoices'][$i]['TermLength'] * 2))
              ) {
                $flag30 = true;
              } elseif (
                ($value['openInvoices'][$i]['InvoiceTerms'] == 1 && (60 <= $diff->days && $diff->days < 90)) ||
                ($value['openInvoices'][$i]['TermLength'] * 2 <= $diff->days &&
                  $diff->days < $value['openInvoices'][$i]['TermLength'] * 3)
              ) {
                $flag60 = true;
              } elseif (
                ($value['openInvoices'][$i]['InvoiceTerms'] == 1 && (90 <= $diff->days && $diff->days < 180)) ||
                ($value['openInvoices'][$i]['TermLength'] * 3 <= $diff->days &&
                  $diff->days < $value['openInvoices'][$i]['TermLength'] * 4)
              ) {
                $flag90 = true;
              } elseif (
                ($value['openInvoices'][$i]['InvoiceTerms'] == 1 && ($diff->days >= 180)) ||
                $diff->days >= $value['openInvoices'][$i]['TermLength'] * 4
              ) {
                $flagover90 = true;
              }
            }
            if ($flag30 === true) {
              $tempInvoice->InvoiceTotal += $value['openInvoices'][$i]['InvoiceSubTotal'];
              if (property_exists($tempInvoice, 'Late30Invoice')) {
                $tempInvoice->Late30Invoice .= (strpos($tempInvoice->Late30Invoice, '+') === false) ? '+' : '';
              } else {
                $tempInvoice->Late30Invoice = $value['openInvoices'][$i]['InvoiceNumber'];
              }
              if (property_exists($tempInvoice, 'Late30Value')) {
                $tempInvoice->Late30Value +=  $value['openInvoices'][$i]['InvoiceSubTotal'];
              } else {
                $tempInvoice->Late30Value = $value['openInvoices'][$i]['InvoiceSubTotal'];
              }
            } elseif ($flag60 === true) {
              $tempInvoice->InvoiceTotal += $value['openInvoices'][$i]['InvoiceSubTotal'];
              if (property_exists($tempInvoice, 'Late60Invoice')) {
                $tempInvoice->Late60Invoice .= (strpos($tempInvoice->Late60Invoice, '+') === false) ? '+' : '';
              } else {
                $tempInvoice->Late60Invoice = $value['openInvoices'][$i]['InvoiceNumber'];
              }
              if (property_exists($tempInvoice, 'Late60Value')) {
                $tempInvoice->Late60Value +=  $value['openInvoices'][$i]['InvoiceSubTotal'];
              } else {
                $tempInvoice->Late60Value = $value['openInvoices'][$i]['InvoiceSubTotal'];
              }
            } elseif ($flag90 === true) {
              $tempInvoice->InvoiceTotal += $value['openInvoices'][$i]['InvoiceSubTotal'];
              if (property_exists($tempInvoice, 'Late90Invoice')) {
                $tempInvoice->Late90Invoice .= (strpos($tempInvoice->Late90Invoice, '+') === false) ? '+' : '';
              } else {
                $tempInvoice->Late90Invoice = $value['openInvoices'][$i]['InvoiceNumber'];
              }
              if (property_exists($tempInvoice, 'Late90Value')) {
                $tempInvoice->Late90Value +=  $value['openInvoices'][$i]['InvoiceSubTotal'];
              } else {
                $tempInvoice->Late90Value = $value['openInvoices'][$i]['InvoiceSubTotal'];
              }
            } elseif ($flagover90 === true) {
              $tempInvoice->InvoiceTotal += $value['openInvoices'][$i]['InvoiceSubTotal'];
              $this->Over90InvoiceList[] = $value['openInvoices'][$i]['InvoiceNumber'];
              if (property_exists($tempInvoice, 'Over90Value')) {
                $tempInvoice->Over90Value +=  $value['openInvoices'][$i]['InvoiceSubTotal'];
              } else {
                $tempInvoice->Over90Value = $value['openInvoices'][$i]['InvoiceSubTotal'];
              }
            }
            // Fix the over90Invioce to display a maximum of 4 invoice numbers or 3 invoice numbers
            // and how many are not displayed, but only if there is at least one
            if (!empty($this->Over90InvoiceList)) {
              if (count($this->Over90InvoiceList) > 4) {
                $j = count($this->Over90InvoiceList) - 3;
                $appendment = ", + $j more";
                $tempInvoice->Over90Invoice = implode(', ', array_slice($this->Over90InvoiceList, 0, 3));
                $tempInvoice->Over90Invoice .= $appendment;
              } else {
                $tempInvoice->Over90Invoice = implode(', ',$this->Over90InvoiceList);
              }
            }
          }
          // Clear this list for the next iteration
          $this->Over90InvoiceList = [];
        }
        // add invoice object to array for submission
        $this->newInvoices[] = $tempInvoice;
      }
    }
    
    private function getTotal($ticketArray, $invoiceNumber) {
      if (!is_array($ticketArray)) return 0;
      $total = 0;
      for ($i = 0; $i < count($ticketArray); $i++) {
        $total += ($ticketArray[$i]['Charge'] !== 0) ? $ticketArray[$i]['TicketPrice'] : 0;
        $this->ticketUpdateKeys[] = $ticketArray[$i]['ticket_index'];
        $temp = new stdClass();
        $temp->InvoiceNumber = $invoiceNumber;
        $this->ticketUpdateValues[] = $temp;
      }
      return $total;
    }
    
    private function submitInvoices() {
      $this->clearParameters();
      $this->payload = $this->newInvoices;
      $this->endPoint = 'invoices';
      $this->method = 'POST';
      $this->buildURI();
      $this->call();
      foreach ($this->result as $i => $index) {
        for($j = 0; $j < count($this->ticketUpdateValues); $j++) {
          if ($this->ticketUpdateValues[$j]->InvoiceNumber === $this->newInvoices[$i]->InvoiceNumber) {
            $this->ticketUpdateValues[$j]->invoice_id = $index;
          }
        }
      }
    }
    
    private function submitTickets() {
      $this->clearParameters();
      $this->primaryKey = implode(',', $this->ticketUpdateKeys);
      $this->payload = $this->ticketUpdateValues;
      $this->endPoint = 'tickets';
      $this->method = 'PUT';
      $this->buildURI();
      $this->call();
    }
  }
      
Return To Top