connectors

object

%Connector

/phlo/resources/connectors/Connector.phlo

Base class for API connectors: credentials, JSON requests, retries, pagination and a normalized result contract

Make one with Connector::make(); it reads its own section from %creds, so keys live in data/creds.ini or in PHLO__Section__key in the environment and never in your code. Every call answers in the same shape, ok with status and data or ok false with error, and nothing is thrown, so test ->ok rather than catching. Raise retries above zero to let GET, HEAD and QUERY back off and try again on 429 and 5xx; writes are never retried, because a repeated POST would book twice.

apiconnectorhttprestbase
const

Connector :: section

line 11
Defines a section within a Connector, allowing for the organization of related components and functionalities.
void
const

Connector :: api

line 12
Defines a connector for API interactions, allowing seamless communication between different services.
void
method

%Connector -> __construct (?array $config = null)

line 14
Initializes a Connector instance with optional configuration settings. If no configuration is provided, it attempts to load credentials from a predefined section.
if ($config === null){
	$section = static::section
	$creds = $section ? %creds->{$section} : null
	$config = $creds ? (array)$creds->toArray : []
}
$this->config = $config
$this->timeout = 15
$this->retries = 0
static

Connector :: make (?array $config = null):static

line 25
Creates a new instance of the Connector class using the provided configuration.
new static($config)
method

%Connector -> base:string

line 27
Accesses the base API for the Connector, allowing interaction with its core functionalities.
static::api
method

%Connector -> headers:array

line 28
This property retrieves the headers associated with the Connector.
[]
static

Connector :: fields:array

line 30
Connector::$fields is a static property that holds an array of field definitions for the Connector class.
[]
method

%Connector -> configured (...$keys):bool

line 32
Checks if all specified keys are configured in the Connector's configuration. Returns true if all keys are present, otherwise returns false.
foreach ($keys AS $key){
	if (($this->config[$key] ?? void) === void) return false
}
return true
method

%Connector -> missing (...$keys):?obj

line 39
Checks if the specified credentials are configured and returns null if they are; otherwise, it triggers a failure indicating which credentials are missing.
return $this->configured(...$keys) ? null : static::fail(static::section.' credentials not configured ('.implode(', ', $keys).')')
static

Connector :: bearer ($token):string

line 43
The Connector::$bearer property holds the authorization header value formatted with a Bearer token for API requests.
'Authorization: Bearer '.$token
static

Connector :: basic ($user, $pass):string

line 44
Generates a Basic Authorization header by encoding the provided username and password in base64 format.
'Authorization: Basic '.base64_encode($user.colon.$pass)
static

Connector :: build (string $method, string $url, ?array $query = null, array $headers = [], mixed $json = null, mixed $form = null):array

line 46
if ($query) $url .= (str_contains($url, qm) ? '&' : qm).http_build_query($query)
$body = null
if ($json !== null){
	$body = is_string($json) ? $json : json_encode($json, jsonFlat)
	$headers[] = 'Content-Type: application/json'
}
elseif ($form !== null){
	$body = is_string($form) ? $form : http_build_query($form)
	$headers[] = 'Content-Type: application/x-www-form-urlencoded'
}
$headers[] = 'Accept: application/json'
return ['method' => strtoupper($method), 'url' => $url, 'headers' => $headers, 'body' => $body]
static

Connector :: ok ($data, int $status = 200):obj

line 61
The Connector::$ok property holds an object with a boolean 'ok' value, a 'status', and 'data' attributes.
obj(ok: true, status: $status, data: $data)
static

Connector :: fail ($error, int $status = 0):obj

line 62
The Connector::$fail property represents a failure state, containing an object with 'ok' set to false, along with the 'status' and 'error' details.
obj(ok: false, status: $status, error: $error)
static

Connector :: errorMessage ($data, string $raw, int $status):string

line 64
Connector::$errorMessage retrieves the error message from a given data object, checking various properties for a string representation of the error, and defaults to a formatted HTTP status message if no error is found.
if (is_object($data)){
	if (isset($data->error->message)) return (string)$data->error->message
	if (isset($data->error) && is_string($data->error)) return $data->error
	if (isset($data->message)) return (string)$data->message
	if (isset($data->errors)){
		$errors = $data->errors
		if (is_string($errors)) return $errors
		if (is_array($errors)) return is_string($errors[0] ?? null) ? $errors[0] : json_encode($errors)
		if (is_object($errors)) return json_encode($errors)
	}
}
return $raw !== void ? $raw : 'HTTP '.$status
static

Connector :: parse ($raw, int $status = 200):obj

line 79
Parses a raw input string into a JSON object, returning a success response or an error message based on the HTTP status code.
$raw = (string)$raw
$data = $raw === void ? null : json_decode($raw)
if ($status < 200 || $status >= 300) return static::fail(static::errorMessage($data, $raw, $status), $status)
return obj(ok: true, status: $status, data: $data ?? $raw)
static

Connector :: retryable ($method, int $status):bool

line 86
in_array($method, ['GET', 'HEAD', 'QUERY']) && ($status === 429 || $status >= 500)
static

Connector :: backoff (int $attempt, $response):int

line 88
The Connector::$backoff property calculates the backoff time in microseconds based on the 'retry-after' header from the response, with a maximum limit of 30 seconds or a default value multiplied by the attempt count.
$after = (int)($response->headers['retry-after'] ?? 0)
return $after > 0 ? min($after, 30) * 1000000 : 200000 * $attempt
method

%Connector -> dispatch (array $req):obj

line 97
Retries only what retryable() allows, and waits as long as the server asked for.
$method = $req['method']
$body = $req['body']
$attempt = 0
$response = null
while (true){
	try {
		if ($method === 'GET') $raw = HTTP($req['url'], $req['headers'], cookies: false, timeout: $this->timeout, response: $response)
		elseif ($method === 'DELETE') $raw = HTTP($req['url'], $req['headers'], DELETE: true, cookies: false, timeout: $this->timeout, response: $response)
		elseif ($method === 'PUT') $raw = HTTP($req['url'], $req['headers'], PUT: $body ?? void, cookies: false, timeout: $this->timeout, response: $response)
		elseif ($method === 'PATCH') $raw = HTTP($req['url'], $req['headers'], PATCH: $body ?? void, cookies: false, timeout: $this->timeout, response: $response)
		elseif ($method === 'QUERY') $raw = HTTP($req['url'], $req['headers'], QUERY: $body ?? void, cookies: false, timeout: $this->timeout, response: $response)
		else $raw = HTTP($req['url'], $req['headers'], POST: $body ?? void, cookies: false, timeout: $this->timeout, response: $response)
	}
	catch (\Throwable $e){
		return static::fail($e->getMessage(), 0)
	}
	$status = $response->status ?? 0
	if (($status >= 200 && $status < 300) || !static::retryable($method, $status) || $attempt >= $this->retries) break
	usleep(static::backoff(++$attempt, $response))
}
$result = static::parse($raw, $status)
$result->headers = $response->headers ?? []
return $result
method

%Connector -> request (string $method, string $url, ?array $query = null, array $headers = [], mixed $json = null, mixed $form = null):obj

line 123
Sends an HTTP request using the specified method, URL, query parameters, headers, and optional JSON or form data.
if (!str_starts_with($url, 'http')) $url = rtrim((string)$this->base, slash).slash.ltrim($url, slash)
$headers = array_merge((array)$this->headers, $headers)
return $this->dispatch(static::build($method, $url, $query, $headers, $json, $form))
method

%Connector -> get (string $url, ?array $query = null, array $headers = []):obj

line 129
Sends a GET request to the specified URL with optional query parameters and headers.
$this->request('GET', $url, query: $query, headers: $headers)
method

%Connector -> post (string $url, mixed $json = null, array $headers = []):obj

line 130
Sends a POST request to the specified URL with optional JSON data and headers.
$this->request('POST', $url, headers: $headers, json: $json)
method

%Connector -> put (string $url, mixed $json = null, array $headers = []):obj

line 131
Sends a PUT request to the specified URL with optional JSON data and headers.
$this->request('PUT', $url, headers: $headers, json: $json)
method

%Connector -> patch (string $url, mixed $json = null, array $headers = []):obj

line 132
Sends a PATCH request to the specified URL with optional JSON data and headers.
$this->request('PATCH', $url, headers: $headers, json: $json)
method

%Connector -> query (string $url, mixed $json = null, array $headers = []):obj

line 133
$this->request('QUERY', $url, headers: $headers, json: $json)
method

%Connector -> del (string $url, array $headers = []):obj

line 134
Sends a DELETE request to the specified URL with optional headers.
$this->request('DELETE', $url, headers: $headers)
method

%Connector -> form (string $url, array $fields, array $headers = []):obj

line 135
Sends a POST request to the specified URL with the given form fields and headers.
$this->request('POST', $url, headers: $headers, form: $fields)
method

%Connector -> paginate (string $url, callable $extract, ?array $query = null, string $param = 'page', int $start = 1, int $max = 0):array

line 137
Fetches paginated data from a specified URL, extracting items using a provided callable and accumulating them until a maximum count is reached or no more data is available.
$items = []
$page = $start
while (true){
	$res = $this->get($url, ($query ?? []) + [$param => $page])
	if (!$res->ok) break
	$batch = $extract($res->data)
	if (!$batch) break
	foreach ($batch AS $item) $items[] = $item
	if ($max && count($items) >= $max) break
	$page++
}
return $items
object

%EBoekhouden

/phlo/resources/connectors/finance/EBoekhouden.phlo

e-Boekhouden.nl connector: session auth from an API token, relations and sales invoices

Authenticates with a session rather than a token per call: the first call trades api_token for one, later calls in the same request reuse it, and a new request starts over. Sessions are limited, so gather the work you need per request instead of making a connector per call.

eboekhoudenaccountinginvoicesrelationsconnector
const

EBoekhouden :: section

line 12
'EBoekhouden'
const

EBoekhouden :: api

line 13
'https://api.e-boekhouden.nl/v1'
prop

%EBoekhouden -> sessionToken:string

line 15
void
method

%EBoekhouden -> headers:array

line 17
$this->sessionToken !== void ? ['Authorization: '.$this->sessionToken] : []
static

EBoekhouden :: fields:array

line 19
arr(
	section: 'EBoekhouden',
	config: arr(source: 'Source label shown in the e-Boekhouden audit trail (optional)'),
	secret: arr(api_token: 'API token (Beheer > Instellingen > API)'),
)
method

%EBoekhouden -> session:obj

line 28
Returns the session token, fetching one on first use.
if ($m = $this->missing('api_token')) return $m
if ($this->sessionToken !== void) return static::ok($this->sessionToken)
$res = $this->post('session', ['accessToken' => trim((string)$this->config['api_token']), 'source' => (string)($this->config['source'] ?? 'Phlo')])
if (!$res->ok) return $res
$token = (string)($res->data->token ?? void)
if ($token === void) return static::fail('e-Boekhouden session token missing in response', $res->status)
$this->sessionToken = $token
return static::ok($token)
method

%EBoekhouden -> guard:?obj

line 39
$res = $this->session
return $res->ok ? null : $res
method

%EBoekhouden -> relations (array $query = []):obj

line 44
if ($m = $this->guard) return $m
return $this->get('relation', $query)
method

%EBoekhouden -> createRelation (array $relation):obj

line 49
if ($m = $this->guard) return $m
return $this->post('relation', $relation)
method

%EBoekhouden -> invoices (array $query = []):obj

line 54
if ($m = $this->guard) return $m
return $this->get('invoice', $query)
method

%EBoekhouden -> createInvoice (array $invoice):obj

line 59
if ($m = $this->guard) return $m
return $this->post('invoice', $invoice)
object

%ExactOnline

/phlo/resources/connectors/finance/ExactOnline.phlo

Exact Online connector (OAuth2): read sales invoices and accounts, create sales invoices

The division number sits in the base URL, so a connector speaks to exactly one administration and a second administration needs a second connector with its own config. Exact rotates the refresh token on every refresh, which is why the token store locks; running two apps on one refresh token logs both out.

exactexactonlineaccountinginvoicesoauthconnector
const

ExactOnline :: section

line 12
Defines a section within the ExactOnline resource, allowing for structured data management.
'ExactOnline'
const

ExactOnline :: tokenUrl

line 13
Returns the URL for obtaining an OAuth2 token from ExactOnline.
'https://start.exactonline.nl/api/oauth2/token'
method

%ExactOnline -> base:string

line 15
Constructs the base URL for the ExactOnline API using the specified division from the configuration.
'https://start.exactonline.nl/api/v1/'.($this->config['division'] ?? void)
static

ExactOnline :: fields:array

line 17
Defines the fields required for integrating with ExactOnline, including configuration for OAuth credentials and scopes.
arr(
	section: 'ExactOnline',
	config: arr(division: 'Division (administration) number'),
	secret: arr(
		client_id: 'OAuth client ID',
		client_secret: 'OAuth client secret',
		refresh_token: 'OAuth refresh token (managed and rotated after first authorization)',
	),
	scopes: 'OAuth2 authorization code flow; token endpoint refreshes automatically',
)
method

%ExactOnline -> guard:?obj

line 28
Checks for the presence of required parameters 'division', 'client_id', 'client_secret', and 'refresh_token'.
$this->missing('division', 'client_id', 'client_secret', 'refresh_token')
method

%ExactOnline -> invoices (array $query = []):obj

line 30
Retrieves a list of sales invoices from ExactOnline based on the provided query parameters.
if ($m = $this->guard) return $m
return $this->get('salesinvoice/SalesInvoices', $query)
method

%ExactOnline -> accounts (array $query = []):obj

line 35
Retrieves account information from ExactOnline using the specified query parameters.
if ($m = $this->guard) return $m
return $this->get('crm/Accounts', $query)
method

%ExactOnline -> createInvoice (array $invoice):obj

line 40
Creates a new invoice in ExactOnline using the provided invoice data.
if ($m = $this->guard) return $m
return $this->post('salesinvoice/SalesInvoices', $invoice)
object

%GoogleCalendar

/phlo/resources/connectors/cloud/GoogleCalendar.phlo

Google Calendar connector (OAuth2): read events and create events

Shares the Google section with Sheets, so one refresh token has to carry both scopes. calendarId defaults to primary, which is the mailbox of the account that authorized, not a shared agenda; give a calendar address for those. Google returns times as RFC3339 with an offset, so keep the timezone rather than casting to a local timestamp.

googlecalendareventsoauthconnector
const

GoogleCalendar :: section

line 12
Defines a section within a Google Calendar resource, allowing for the organization of events and details.
'Google'
const

GoogleCalendar :: tokenUrl

line 13
This is the URL endpoint used to obtain an access token from Google Calendar's OAuth 2.0 authorization server.
'https://oauth2.googleapis.com/token'
method

%GoogleCalendar -> base:string

line 15
This defines the base URL for accessing the Google Calendar API version 3.
'https://www.googleapis.com/calendar/v3'
static

GoogleCalendar :: fields:array

line 17
GoogleCalendar::$fields provides configuration details for accessing Google Calendar, including OAuth credentials and required scopes.
arr(
	section: 'Google',
	secret: arr(
		client_id: 'OAuth client ID',
		client_secret: 'OAuth client secret',
		refresh_token: 'OAuth refresh token (scopes: calendar, spreadsheets)',
	),
	scopes: 'https://www.googleapis.com/auth/calendar, https://www.googleapis.com/auth/spreadsheets',
)
method

%GoogleCalendar -> guard:?obj

line 27
Checks for the presence of required credentials: client_id, client_secret, and refresh_token.
$this->missing('client_id', 'client_secret', 'refresh_token')
method

%GoogleCalendar -> events (string $calendarId = 'primary', array $query = []):obj

line 29
Retrieves events from a specified Google Calendar using the provided calendar ID and optional query parameters.
if ($m = $this->guard) return $m
return $this->get('calendars/'.rawurlencode($calendarId).'/events', $query)
method

%GoogleCalendar -> createEvent (array $event, string $calendarId = 'primary'):obj

line 34
Creates a new event in the specified Google Calendar using the provided event details.
if ($m = $this->guard) return $m
return $this->post('calendars/'.rawurlencode($calendarId).'/events', $event)
object

%GoogleSheets

/phlo/resources/connectors/cloud/GoogleSheets.phlo

Google Sheets connector (OAuth2): read ranges and append rows

Shares the Google section with Calendar, so one refresh token has to carry both scopes. A range is A1 notation including the tab name, e.g. Sheet1!A:D. append() writes with USER_ENTERED, so the sheet parses what you send just as a typist would and a leading zero or a date-like string is reinterpreted; pass RAW when the value has to stay untouched.

googlesheetsspreadsheetoauthconnector
const

GoogleSheets :: section

line 12
Defines a section for organizing data within a Google Sheets resource.
'Google'
const

GoogleSheets :: tokenUrl

line 13
This is the URL endpoint for obtaining OAuth 2.0 tokens from Google Sheets.
'https://oauth2.googleapis.com/token'
method

%GoogleSheets -> base:string

line 15
This item represents a base URL for accessing the Google Sheets API, specifically the endpoint for interacting with spreadsheets.
'https://sheets.googleapis.com/v4/spreadsheets'
static

GoogleSheets :: fields:array

line 17
Defines the configuration fields for Google Sheets integration, including OAuth credentials and required scopes.
arr(
	section: 'Google',
	secret: arr(
		client_id: 'OAuth client ID',
		client_secret: 'OAuth client secret',
		refresh_token: 'OAuth refresh token (scopes: calendar, spreadsheets)',
	),
	scopes: 'https://www.googleapis.com/auth/spreadsheets',
)
method

%GoogleSheets -> guard:?obj

line 27
Checks for the presence of required credentials: client_id, client_secret, and refresh_token.
$this->missing('client_id', 'client_secret', 'refresh_token')
method

%GoogleSheets -> values ($spreadsheetId, string $range):obj

line 29
Retrieves the values from a specified range in a Google Sheets spreadsheet identified by the given spreadsheet ID.
if ($m = $this->guard) return $m
return $this->get($spreadsheetId.'/values/'.rawurlencode($range))
method

%GoogleSheets -> append ($spreadsheetId, string $range, array $rows, string $valueInputOption = 'USER_ENTERED'):obj

line 34
Appends rows of data to a specified range in a Google Sheets spreadsheet, using the provided value input option.
if ($m = $this->guard) return $m
return $this->post($spreadsheetId.'/values/'.rawurlencode($range).':append?valueInputOption='.$valueInputOption, ['values' => $rows])
object

%Lightspeed

/phlo/resources/connectors/shops/Lightspeed.phlo

Lightspeed Retail (V3) connector: read customers and sales, create customers

Retail V3, so cluster_id names the account and the key and secret authenticate. Lightspeed rate limits per account with a leaky bucket, so raise retries rather than firing calls in a loop. findCustomer() picks its search field from what you pass: an address with an at sign searches on email, anything else on phone.

lightspeedwebshopretailposcustomersconnector
const

Lightspeed :: section

line 12
Defines a section in a Lightspeed application, allowing for the organization and management of content within that section.
'Lightspeed'
method

%Lightspeed -> base:string

line 14
Lightspeed->base constructs the base URL for accessing the Lightspeed API, incorporating the cluster ID from the configuration if available.
'https://api.lightspeedapp.com/API/V3/Account/'.($this->config['cluster_id'] ?? void)
method

%Lightspeed -> headers:array

line 16
This method retrieves the headers required for Lightspeed API authentication using the provided API key and secret from the configuration.
[static::basic($this->config['api_key'] ?? void, $this->config['api_secret'] ?? void)]
static

Lightspeed :: fields:array

line 18
Lightspeed::$fields defines the configuration fields required for connecting to the Lightspeed API, including cluster ID, language, API key, API secret, and access scopes.
arr(
	section: 'Lightspeed',
	config: arr(
		cluster_id: 'Account / cluster ID',
		language: 'Language (optional, default nl)',
	),
	secret: arr(
		api_key: 'API key',
		api_secret: 'API secret',
	),
	scopes: 'Customer read/write, Sale read',
)
method

%Lightspeed -> customers (array $query = []):obj

line 31
Retrieves customer data from the Lightspeed API using the provided query parameters.
if ($m = $this->missing('cluster_id', 'api_key', 'api_secret')) return $m
return $this->get('Customer.json', $query)
method

%Lightspeed -> findCustomer ($participant):obj

line 36
Finds a customer by their email or phone number, depending on the format of the provided participant identifier.
if ($m = $this->missing('cluster_id', 'api_key', 'api_secret')) return $m
$field = str_contains((string)$participant, '@') ? 'Email' : 'Phone'
return $this->get('Customer.json', [$field => $participant, 'limit' => 1])
method

%Lightspeed -> customer ($id):obj

line 42
Retrieves customer information from the Lightspeed API using the specified customer ID.
if ($m = $this->missing('cluster_id', 'api_key', 'api_secret')) return $m
return $this->get('Customer/'.$id.'.json')
method

%Lightspeed -> sales (array $query = []):obj

line 47
Retrieves sales data from the API using the provided query parameters, ensuring that required credentials are present.
if ($m = $this->missing('cluster_id', 'api_key', 'api_secret')) return $m
return $this->get('Sale.json', $query)
method

%Lightspeed -> createCustomer (array $customer):obj

line 52
Creates a new customer by sending the provided customer data to the Lightspeed API, ensuring that required parameters are present.
if ($m = $this->missing('cluster_id', 'api_key', 'api_secret')) return $m
return $this->post('Customer.json', $customer)
object

%MessageBird

/phlo/resources/connectors/chat/MessageBird.phlo

MessageBird connector: send SMS

Needs access_key and an originator, the sender shown to the recipient; some countries refuse an alphanumeric originator, so a real number is the safer choice. Pass an array as the recipient to send one message to several numbers at once.

messagebirdsmsmessagingconnector
const

MessageBird :: section

line 12
Defines a section for MessageBird within the Phlo application.
'MessageBird'
method

%MessageBird -> base:string

line 14
This defines the base URL for the MessageBird API, which is used for sending and receiving messages.
'https://rest.messagebird.com'
method

%MessageBird -> headers:array

line 16
This retrieves the headers required for authenticating requests to the MessageBird API using the provided access key.
['Authorization: AccessKey '.($this->config['access_key'] ?? void)]
static

MessageBird :: fields:array

line 18
Defines the fields for the MessageBird configuration, including the originator and access key.
arr(
	section: 'MessageBird',
	config: arr(originator: 'Originator (sender number or name)'),
	secret: arr(access_key: 'Access key'),
)
static

MessageBird :: errorMessage ($data, string $raw, int $status):string

line 24
Retrieves the error message from the MessageBird API response, if available; otherwise, it falls back to the parent class's error message method.
if (is_object($data) && isset($data->errors[0]->description)) return (string)$data->errors[0]->description
return parent::errorMessage($data, $raw, $status)
method

%MessageBird -> sms ($to, $body, array $extra = []):obj

line 29
Sends an SMS message to the specified recipient(s) using the MessageBird API, requiring an access key and originator configuration.
if ($m = $this->missing('access_key', 'originator')) return $m
$recipients = is_array($to) ? $to : [$to]
return $this->post('messages', ['originator' => $this->config['originator'], 'recipients' => $recipients, 'body' => $body] + $extra)
object

%MicrosoftGraph

/phlo/resources/connectors/cloud/MicrosoftGraph.phlo

Microsoft Graph connector (app-only client credentials): read users and calendars, send mail, create events

This is the app-only flow: the token belongs to the registration, not to a person, so it needs admin-consented application permissions and every call names the mailbox it acts on. Tokens are cached in APCu for their lifetime, so a run without APCu fetches one per request. Set mailbox to spare yourself passing a user each time.

microsoftgraphoffice365calendarmailconnector
const

MicrosoftGraph :: section

line 12
This function interacts with the Microsoft Graph API to manage sections within a resource.
'Microsoft'
method

%MicrosoftGraph -> base:string

line 14
This defines the base URL for accessing the Microsoft Graph API, allowing interaction with Microsoft services and resources.
'https://graph.microsoft.com/v1.0'
method

%MicrosoftGraph -> headers:array

line 16
This item retrieves the headers required for authentication with Microsoft Graph using a bearer token.
[static::bearer((string)$this->token)]
static

MicrosoftGraph :: fields:array

line 18
Defines the configuration fields required to connect to Microsoft Graph, including tenant ID, client ID, mailbox, client secret, and application permissions.
arr(
	section: 'Microsoft',
	config: arr(
		tenant_id: 'Azure AD tenant ID',
		client_id: 'App registration client ID',
		mailbox: 'Default mailbox / user UPN for calendar and mail (optional)',
	),
	secret: arr(client_secret: 'App registration client secret'),
	scopes: 'Application permissions: User.Read.All, Calendars.ReadWrite, Mail.Send',
)
prop

%MicrosoftGraph -> token:string

line 29
Retrieves an access token from Microsoft Graph using the fetchToken method.
$this->fetchToken()
method

%MicrosoftGraph -> fetchToken:string

line 35
Caches the app-only token in APCu until a minute before it expires.
$tenant = $this->config['tenant_id'] ?? void
$id = $this->config['client_id'] ?? void
$secret = $this->config['client_secret'] ?? void
if ($tenant === void || $id === void || $secret === void) return void
$key = 'phlo:graph:'.$tenant.colon.$id
if (function_exists('apcu_fetch')){
	$cached = apcu_fetch($key)
	if ($cached) return $cached
}
$res = $this->dispatch(static::build('POST', 'https://login.microsoftonline.com/'.$tenant.'/oauth2/v2.0/token', null, [], null, ['grant_type' => 'client_credentials', 'client_id' => $id, 'client_secret' => $secret, 'scope' => 'https://graph.microsoft.com/.default']))
if (!$res->ok) return void
$token = $res->data->access_token ?? void
$expires = (int)($res->data->expires_in ?? 3600)
if ($token !== void && function_exists('apcu_store')) apcu_store($key, $token, max(60, $expires - 60))
return $token
method

%MicrosoftGraph -> mailbox ($user = null):string

line 53
Retrieves the mailbox associated with a specified user, or defaults to a configured mailbox if no user is provided.
$user ?? ($this->config['mailbox'] ?? void)
method

%MicrosoftGraph -> users (array $query = []):obj

line 55
Retrieves a list of users from Microsoft Graph based on the provided query parameters.
if ($m = $this->missing('tenant_id', 'client_id', 'client_secret')) return $m
return $this->get('users', $query)
method

%MicrosoftGraph -> user ($id):obj

line 60
Fetches user information from Microsoft Graph using the specified user ID.
if ($m = $this->missing('tenant_id', 'client_id', 'client_secret')) return $m
return $this->get('users/'.rawurlencode((string)$id))
method

%MicrosoftGraph -> events ($user = null, array $query = []):obj

line 65
Fetches events from a specified user's Microsoft mailbox using the Microsoft Graph API.
if ($m = $this->missing('tenant_id', 'client_id', 'client_secret')) return $m
$mailbox = $this->mailbox($user)
if ($mailbox === void) return static::fail('Microsoft mailbox required')
return $this->get('users/'.rawurlencode((string)$mailbox).'/events', $query)
method

%MicrosoftGraph -> sendMail ($message, $user = null, bool $save = true):obj

line 72
Sends an email message using the Microsoft Graph API, optionally specifying a user and whether to save the message to the Sent Items folder.
if ($m = $this->missing('tenant_id', 'client_id', 'client_secret')) return $m
$mailbox = $this->mailbox($user)
if ($mailbox === void) return static::fail('Microsoft mailbox required')
return $this->post('users/'.rawurlencode((string)$mailbox).'/sendMail', ['message' => $message, 'saveToSentItems' => $save])
method

%MicrosoftGraph -> createEvent (array $event, $user = null):obj

line 79
Creates a new event in the specified user's Microsoft calendar using the provided event details.
if ($m = $this->missing('tenant_id', 'client_id', 'client_secret')) return $m
$mailbox = $this->mailbox($user)
if ($mailbox === void) return static::fail('Microsoft mailbox required')
return $this->post('users/'.rawurlencode((string)$mailbox).'/events', $event)
object

%Moneybird

/phlo/resources/connectors/finance/Moneybird.phlo

Moneybird connector: read contacts and invoices, create sales invoices

Needs an administration_id and a personal access token with rights for contacts and invoices. findContact() searches on one match, so use it to look up rather than to list. An invoice is created as a draft: sending or booking it is a separate step in Moneybird.

moneybirdaccountinginvoicescontactsconnector
const

Moneybird :: section

line 12
Defines a section within the Moneybird integration for organizing related functionalities.
'Moneybird'
method

%Moneybird -> base:string

line 14
This retrieves the base URL for the Moneybird API, incorporating the administration ID from the configuration if available.
'https://moneybird.com/api/v2/'.($this->config['administration_id'] ?? void)
method

%Moneybird -> headers:array

line 16
Returns the headers required for authentication with the Moneybird API, using a bearer token derived from the access token in the configuration.
[static::bearer($this->config['access_token'] ?? void)]
static

Moneybird :: fields:array

line 18
Defines the fields for the Moneybird integration, including the administration ID and a personal access token for accessing contacts and invoices.
arr(
	section: 'Moneybird',
	config: arr(administration_id: 'Administration ID'),
	secret: arr(access_token: 'Personal access token with read/write for contacts and invoices'),
)
method

%Moneybird -> contacts (array $query = []):obj

line 24
Retrieves a list of contacts from Moneybird based on the provided query parameters.
if ($m = $this->missing('administration_id', 'access_token')) return $m
return $this->get('contacts.json', $query)
method

%Moneybird -> findContact ($query):obj

line 29
Finds a contact in Moneybird based on the provided query, returning the first result found.
if ($m = $this->missing('administration_id', 'access_token')) return $m
return $this->get('contacts.json', ['query' => $query, 'per_page' => 1])
method

%Moneybird -> contact ($id):obj

line 34
Retrieves the contact information associated with the specified ID from Moneybird, requiring an administration ID and access token.
if ($m = $this->missing('administration_id', 'access_token')) return $m
return $this->get('contacts/'.$id.'.json')
method

%Moneybird -> invoices (array $query = []):obj

line 39
Fetches invoices from Moneybird based on the provided query parameters, requiring an 'administration_id' and 'access_token'.
if ($m = $this->missing('administration_id', 'access_token')) return $m
return $this->get('sales_invoices.json', $query)
method

%Moneybird -> createContact (array $contact):obj

line 44
Creates a new contact in Moneybird using the provided contact details. It requires an administration ID and access token to function properly.
if ($m = $this->missing('administration_id', 'access_token')) return $m
return $this->post('contacts.json', ['contact' => $contact])
method

%Moneybird -> createInvoice (array $invoice):obj

line 49
Creates a new invoice in Moneybird using the provided invoice data.
if ($m = $this->missing('administration_id', 'access_token')) return $m
return $this->post('sales_invoices.json', ['sales_invoice' => $invoice])
object

%OAuthConnector

/phlo/resources/connectors/OAuthConnector.phlo

Base class for OAuth2 connectors: stored, auto-refreshed bearer access tokens via TokenStore, on the OAuth2 primitive

Use this instead of Connector when the provider hands out access tokens that expire. Give it client_id, client_secret and a refresh_token and TokenStore keeps the access token and renews it in time; a subclass only names its section and tokenUrl.

oauthoauth2connectorbasetokenrefresh
const

OAuthConnector :: tokenUrl

line 12
Specifies the URL for obtaining an OAuth token.
void
method

%OAuthConnector -> oauthKey:string

line 14
Retrieves the OAuth key for the current section in the OAuthConnector.
static::section
prop

%OAuthConnector -> token:?string

line 16
Retrieves the OAuth token using the provided credentials and refresh token from the TokenStore.
TokenStore::access($this->oauthKey, static::tokenUrl, $this->config['client_id'] ?? void, $this->config['client_secret'] ?? void, ['refresh_token' => $this->config['refresh_token'] ?? null])
method

%OAuthConnector -> headers:array

line 18
Returns the headers required for OAuth authentication, including a bearer token derived from the instance's token.
[static::bearer((string)$this->token)]
method

%OAuthConnector -> authed:bool

line 20
Checks if the OAuth token is authenticated by verifying that it is not void.
(string)$this->token !== void
object

%Resend

/phlo/resources/connectors/chat/Resend.phlo

Resend connector: send transactional email via the HTTP API

Needs api_key and a from_email on a domain you verified with Resend, otherwise the send is refused. send() takes HTML; leave it out for a plain text mail and add anything else Resend accepts through extra.

resendemailtransactionalmessagingconnector
const

Resend :: section

line 12
Resend::section allows you to define a section in a Resend resource for organizing related content.
'Resend'
method

%Resend -> base:string

line 14
Sets the base URL for the Resend API.
'https://api.resend.com'
method

%Resend -> headers:array

line 16
Sets the headers for the Resend request using the provided API key from the configuration.
[static::bearer($this->config['api_key'] ?? void)]
static

Resend :: fields:array

line 18
Defines the fields for the Resend configuration, including the default sender email and the API key.
arr(
	section: 'Resend',
	config: arr(from_email: 'Default sender, e.g. "App <noreply@yourdomain.com>"'),
	secret: arr(api_key: 'API key (re_...)'),
)
method

%Resend -> send ($to, $subject, $html = void, array $extra = []):obj

line 24
Sends an email to the specified recipient(s) with the given subject and optional HTML content, using the configured sender email address.
if ($m = $this->missing('api_key', 'from_email')) return $m
$email = ['from' => $this->config['from_email'], 'to' => is_array($to) ? $to : [$to], 'subject' => $subject]
if ($html !== void) $email['html'] = $html
return $this->post('emails', $email + $extra)
object

%Shopify

/phlo/resources/connectors/shops/Shopify.phlo

Shopify Admin API connector: read customers, orders and products; create draft orders and products; update inventory

Needs shop_domain including myshopify.com and an admin access token; api_version defaults to 2024-01 and a Shopify version is supported for a limited time, so set the one you tested against rather than leaning on the default. setInventory() writes an absolute level, not a difference, so read the current one first when you mean to add. A draft order is not an order until it is completed in Shopify.

shopifywebshopecommerceordersproductsconnector
const

Shopify :: section

line 12
Defines a section in a Shopify theme, allowing for customizable content and layout within the store.
'Shopify'
method

%Shopify -> base:string

line 14
Constructs the base URL for the Shopify API using the shop domain and API version from the configuration.
'https://'.($this->config['shop_domain'] ?? void).'/admin/api/'.($this->config['api_version'] ?? '2024-01')
method

%Shopify -> headers:array

line 16
This retrieves the headers required for Shopify API authentication, including the access token from the configuration.
['X-Shopify-Access-Token: '.($this->config['access_token'] ?? void)]
static

Shopify :: fields:array

line 18
Defines the configuration fields for integrating with Shopify, including shop domain, API version, access token, and required scopes.
arr(
	section: 'Shopify',
	config: arr(
		shop_domain: 'Shop domain, e.g. your-store.myshopify.com',
		api_version: 'Admin API version (optional, default 2024-01)',
	),
	secret: arr(access_token: 'Admin API access token (shpat_...)'),
	scopes: 'read_customers, read_orders, read_products, write_draft_orders, write_inventory',
)
method

%Shopify -> customers (array $query = []):obj

line 28
Fetches a list of customers from Shopify using the provided query parameters.
if ($m = $this->missing('shop_domain', 'access_token')) return $m
return $this->get('customers.json', $query)
method

%Shopify -> searchCustomers ($query, int $limit = 10):obj

line 33
Searches for customers in Shopify based on a query string and an optional limit on the number of results returned.
if ($m = $this->missing('shop_domain', 'access_token')) return $m
return $this->get('customers/search.json', ['query' => $query, 'limit' => $limit])
method

%Shopify -> customer ($id):obj

line 38
Retrieves customer information from Shopify using the specified customer ID.
if ($m = $this->missing('shop_domain', 'access_token')) return $m
return $this->get('customers/'.$id.'.json')
method

%Shopify -> orders (array $query = []):obj

line 43
Fetches a list of orders from Shopify based on the provided query parameters.
if ($m = $this->missing('shop_domain', 'access_token')) return $m
return $this->get('orders.json', $query)
method

%Shopify -> products (array $query = []):obj

line 48
Fetches a list of products from Shopify using the provided query parameters.
if ($m = $this->missing('shop_domain', 'access_token')) return $m
return $this->get('products.json', $query)
method

%Shopify -> createDraftOrder (array $order):obj

line 53
Creates a draft order in Shopify using the provided order details.
if ($m = $this->missing('shop_domain', 'access_token')) return $m
return $this->post('draft_orders.json', ['draft_order' => $order])
method

%Shopify -> createProduct (array $product):obj

line 58
Creates a new product in Shopify using the provided product details.
if ($m = $this->missing('shop_domain', 'access_token')) return $m
return $this->post('products.json', ['product' => $product])
method

%Shopify -> setInventory ($inventoryItemId, $locationId, int $available):obj

line 63
Sets the available inventory level for a specific inventory item at a given location in Shopify.
if ($m = $this->missing('shop_domain', 'access_token')) return $m
return $this->post('inventory_levels/set.json', ['inventory_item_id' => $inventoryItemId, 'location_id' => $locationId, 'available' => $available])
object

%Slack

/phlo/resources/connectors/chat/Slack.phlo

Slack connector: post messages, read channel history and list channels

Needs a bot token, and the bot has to be invited into the channel it posts in, else the call comes back ok false with not_in_channel. Slack answers HTTP 200 even when it refuses, so read the outcome from the result, never from the status.

slackmessagingchatconnector
const

Slack :: section

line 12
Creates a section block in a Slack message, allowing for the inclusion of text and other elements.
'Slack'
const

Slack :: api

line 13
This item provides access to the Slack API, allowing integration with Slack services and functionalities.
'https://slack.com/api'
method

%Slack -> headers:array

line 15
Returns the headers required for authentication when making requests to the Slack API using a bearer token.
[static::bearer($this->config['bot_token'] ?? void)]
static

Slack :: fields:array

line 17
Defines the configuration fields for integrating with Slack, including the bot token, signing secret, and required scopes.
arr(
	section: 'Slack',
	secret: arr(
		bot_token: 'Bot user OAuth token (xoxb-...)',
		signing_secret: 'Signing secret for inbound webhook verification (optional)',
	),
	scopes: 'chat:write, channels:history, channels:read',
)
method

%Slack -> result (obj $res):obj

line 26
Processes the result from a Slack API call, returning the response if successful or a failure message if an error occurred.
if (!$res->ok) return $res
$data = $res->data
if (is_object($data) && ($data->ok ?? null) === false) return static::fail($data->error ?? 'Slack API error', $res->status)
return $res
method

%Slack -> send ($channel, $text, array $extra = []):obj

line 33
Sends a message to a specified Slack channel using the Slack API. It requires a configured bot token and accepts additional parameters through an array.
if (!$this->configured('bot_token')) return static::fail('Slack bot_token not configured')
return $this->result($this->post('chat.postMessage', ['channel' => $channel, 'text' => $text] + $extra))
method

%Slack -> history ($channel, int $limit = 20):obj

line 38
Fetches the message history from a specified Slack channel, with an optional limit on the number of messages returned.
if (!$this->configured('bot_token')) return static::fail('Slack bot_token not configured')
return $this->result($this->get('conversations.history', ['channel' => $channel, 'limit' => $limit]))
method

%Slack -> channels (int $limit = 100, string $types = 'public_channel'):obj

line 43
Retrieves a list of channels from Slack, with optional parameters to limit the number of channels and specify the types of channels to return.
if (!$this->configured('bot_token')) return static::fail('Slack bot_token not configured')
return $this->result($this->get('conversations.list', ['limit' => $limit, 'types' => $types]))
object

%Telegram

/phlo/resources/connectors/chat/Telegram.phlo

Telegram Bot API connector: send messages, photos and documents; poll updates

Needs a bot token from BotFather and a chat_id, and a person has to have written to the bot before it can write to them. Like Slack, the API answers 200 on refusal, which the connector already translates into ok false. photo() and document() take a URL or an existing file_id, not raw bytes.

telegrambotmessagingchatconnector
const

Telegram :: section

line 12
Defines a section within a Telegram resource, allowing for organized grouping of related content.
'Telegram'
method

%Telegram -> base:string

line 14
Constructs the base URL for the Telegram Bot API using the provided bot token from the configuration.
'https://api.telegram.org/bot'.($this->config['bot_token'] ?? void)
static

Telegram :: fields:array

line 16
Defines the fields related to Telegram configuration, including the bot token and optional webhook secret for verification.
arr(
	section: 'Telegram',
	secret: arr(
		bot_token: 'Bot token from BotFather',
		webhook_secret: 'Webhook secret token for inbound verification (optional)',
	),
	help: 'Create a bot via BotFather and store its token. The chat_id is the recipient or chat to message.',
)
method

%Telegram -> result (obj $res):obj

line 25
Processes the result from a Telegram API response, returning the response if successful or a failure message if an error is detected.
if (!$res->ok) return $res
$data = $res->data
if (is_object($data) && ($data->ok ?? null) === false) return static::fail($data->description ?? 'Telegram API error', $res->status)
return $res
method

%Telegram -> send ($chatId, $text, array $extra = []):obj

line 32
Sends a message to a specified Telegram chat using the provided chat ID and text, with optional extra parameters.
if ($m = $this->missing('bot_token')) return $m
return $this->result($this->post('sendMessage', ['chat_id' => $chatId, 'text' => $text] + $extra))
method

%Telegram -> photo ($chatId, $photo, $caption = void, array $extra = []):obj

line 37
Sends a photo to a specified Telegram chat with an optional caption and additional parameters.
if ($m = $this->missing('bot_token')) return $m
$payload = ['chat_id' => $chatId, 'photo' => $photo]
if ($caption !== void) $payload['caption'] = $caption
return $this->result($this->post('sendPhoto', $payload + $extra))
method

%Telegram -> document ($chatId, $document, $caption = void, array $extra = []):obj

line 44
Sends a document to a specified chat on Telegram, optionally including a caption and additional parameters.
if ($m = $this->missing('bot_token')) return $m
$payload = ['chat_id' => $chatId, 'document' => $document]
if ($caption !== void) $payload['caption'] = $caption
return $this->result($this->post('sendDocument', $payload + $extra))
method

%Telegram -> updates (int $offset = 0, int $limit = 100):obj

line 51
Retrieves updates from the Telegram bot API, allowing for pagination through the 'offset' and 'limit' parameters.
if ($m = $this->missing('bot_token')) return $m
return $this->result($this->get('getUpdates', ['offset' => $offset, 'limit' => $limit]))
object

%TokenStore

/phlo/resources/connectors/TokenStore.phlo

Persisted OAuth2 token store with automatic refresh via the OAuth2 resource

Tokens are kept as one file per key under data/tokens with 0600 rights, so a refresh survives a restart and every worker shares it. A refresh takes an exclusive lock, which matters with providers that rotate the refresh token: without it the losing request is left holding a dead one.

oauthoauth2tokenrefreshstorecredentials
static

TokenStore :: path ($key):string

line 11
The TokenStore::$path property defines the file path for storing token data, formatted as a JSON file based on a sanitized version of the provided key.
data.'tokens/'.preg_replace('/[^a-z0-9_.-]+/i', us, (string)$key).'.json'
static

TokenStore :: read ($key):array

line 13
Reads a token from the TokenStore by its key, returning an array of tokens if the file exists and is accessible.
$file = static::path($key)
if (!is_file($file)) return []
@chmod(data.'tokens', 0700)
@chmod($file, 0600)
return (array)json_read($file, true)
static

TokenStore :: write ($key, array $token):void

line 21
This method writes a token to a specified file in a secure directory, ensuring the directory and file have appropriate permissions.
$dir = data.'tokens'
is_dir($dir) || mkdir($dir, 0700, true)
@chmod($dir, 0700)
$file = static::path($key)
json_write($file, $token)
@chmod($file, 0600)
static

TokenStore :: valid (array $token):bool

line 30
Checks if the access token is valid by ensuring it exists and has not expired, allowing for a 30-second buffer before expiration.
($token['access_token'] ?? void) !== void && (int)($token['expires_at'] ?? 0) > time() + 30
static

TokenStore :: store ($res, $refresh):array

line 32
Stores the access token, refresh token, and expiration time for authentication purposes.
$token = [
	'access_token' => $res['access_token'],
	'refresh_token' => $res['refresh_token'] ?? $refresh,
	'expires_at' => time() + (int)($res['expires_in'] ?? 3600),
]
return $token
static

TokenStore :: lock ($key)

line 44
Takes an exclusive lock around one key's refresh cycle.
$dir = data.'tokens'
is_dir($dir) || mkdir($dir, 0700, true)
$lock = @fopen(static::path($key).'.lock', 'c')
if (!$lock || !flock($lock, LOCK_EX)){
	$lock && fclose($lock)
	return null
}
return $lock
static

TokenStore :: access ($key, $tokenUrl, $clientId, $clientSecret, array $seed = []):?string

line 55
Retrieves the access token from the TokenStore, refreshing it if necessary using the refresh token.
$token = static::read($key)
if (static::valid($token)) return $token['access_token']
$lock = static::lock($key)
if (!$lock) return null
try {
	$token = static::read($key)
	if (!($token['refresh_token'] ?? null) && ($seed['refresh_token'] ?? null)){
		$token = ['refresh_token' => $seed['refresh_token']]
		static::write($key, $token)
	}
	if (static::valid($token)) return $token['access_token']
	$refresh = $token['refresh_token'] ?? null
	if (!$refresh || !$tokenUrl || !$clientId) return null
	$res = OAuth2::refresh($tokenUrl, $clientId, $clientSecret, $refresh)
	if (!($res['access_token'] ?? null)) return null
	$token = static::store($res, $refresh)
	static::write($key, $token)
	return $token['access_token']
} finally {
	flock($lock, LOCK_UN)
	fclose($lock)
}
object

%Twilio

/phlo/resources/connectors/chat/Twilio.phlo

Twilio connector: send SMS and read message status

Needs account_sid and auth_token, plus either a from_number in E.164 or a messaging_service_sid; without one of the two the send is refused before it leaves. Twilio speaks form encoding rather than JSON, which sms() handles, so pass extra fields in Twilio's own capitalized names. A returned sid says it was accepted, not delivered; message() tells you what became of it.

twiliosmsmessagingconnector
const

Twilio :: section

line 12
Defines a section for Twilio integration within a Phlo application.
'Twilio'
method

%Twilio -> base:string

line 14
Constructs the base URL for accessing the Twilio API using the account SID from the configuration.
'https://api.twilio.com/2010-04-01/Accounts/'.($this->config['account_sid'] ?? void)
method

%Twilio -> headers:array

line 16
Retrieves the headers required for Twilio API requests using the provided account SID and auth token from the configuration.
[static::basic($this->config['account_sid'] ?? void, $this->config['auth_token'] ?? void)]
static

Twilio :: fields:array

line 18
Defines the fields for configuring Twilio integration, including account SID, sender number, and optional messaging service SID, along with the authentication token.
arr(
	section: 'Twilio',
	config: arr(
		account_sid: 'Account SID (ACxxxx)',
		from_number: 'Sender number in E.164, e.g. +31600000000 (use this or messaging_service_sid)',
		messaging_service_sid: 'Messaging Service SID (optional alternative to from_number)',
	),
	secret: arr(auth_token: 'Auth token'),
)
method

%Twilio -> sms ($to, $body, array $extra = []):obj

line 28
Sends an SMS message using Twilio's API, requiring either a from_number or messaging_service_sid from the configuration.
if ($m = $this->missing('account_sid', 'auth_token')) return $m
$from = $this->config['from_number'] ?? void
$service = $this->config['messaging_service_sid'] ?? void
if ($from === void && $service === void) return static::fail('Twilio from_number or messaging_service_sid required')
$fields = ['To' => $to, 'Body' => $body] + $extra
if ($service !== void) $fields['MessagingServiceSid'] = $service
else $fields['From'] = $from
return $this->form('Messages.json', $fields)
method

%Twilio -> message ($sid):obj

line 39
Retrieves a message from Twilio using the specified message SID.
if ($m = $this->missing('account_sid', 'auth_token')) return $m
return $this->get('Messages/'.$sid.'.json')

Last updated on 7 August 2026

We use essential cookies to make this site work. With your permission we also use analytics to improve the site.