ExoFattura integration
With the exofattura plugin, ExoFattura customers (individuals and businesses) join the phone system's company directory and stay up to date: when a customer calls, their name shows on the phone. The integration is a paid plugin enabled by Exobyte: without it the endpoints return 403 PLUGIN_REQUIRED (disconnecting is always allowed). Read with the contacts:read scope, manage with integrations:write (company administrator; the superadmin uses X-Tenant-Id).
Connecting
# Collega ExoFattura (la chiave viene verificata prima di salvare e non viene più restituita)
curl -s -X PUT https://centralino.exovoip.it/api/v1/integrations/exofattura \
-H "X-API-Key: $EXOPBX_API_KEY" -H "Content-Type: application/json" \
-d '{"apiKey":"'"$EXOFATTURA_API_KEY"'","environment":"production","companyId":7}'
# Stato: ultimo aggiornamento, clienti importati, modalità (realtime / polling)
curl -s https://centralino.exovoip.it/api/v1/integrations/exofattura -H "X-API-Key: $EXOPBX_API_KEY"const { companies } = await pbx.exofattura.companies({
apiKey: process.env.EXOFATTURA_API_KEY!,
environment: 'production',
});
const { integration } = await pbx.exofattura.connect({
apiKey: process.env.EXOFATTURA_API_KEY!,
environment: 'production',
companyId: companies[0]!.id,
});
console.log(integration?.status, integration?.mode, integration?.stats.contacts);$out = $pbx->exofattura->connect([
'apiKey' => getenv('EXOFATTURA_API_KEY'),
'environment' => 'production',
'companyId' => 7,
]);
echo $out['integration']['status'], ' ', $out['integration']['mode'], PHP_EOL;import os
out = pbx.exofattura.connect(
{"apiKey": os.environ["EXOFATTURA_API_KEY"], "environment": "production", "companyId": 7}
)
print(out["integration"]["status"], out["integration"]["mode"])apiKey: ExoFattura API key (fat_…for production,fat_test_…for the test environment). It is checked with a test call before saving, stored encrypted and never returned: responses only carrykeyHint(last characters). OmitapiKeyto change the connection while keeping the key.environment:production(https://api.exofattura.it) ortest(https://api-test.exofattura.it). These are the only allowed addresses: free URLs cannot be set.companyId: ExoFattura company (X-Company-Idheader).POST /v1/integrations/exofattura/companiesreturns the companies available for a key, to offer them in a menu; withavailable: falsethe id is entered by hand.- Invalid, revoked or wrong-environment key:
400 VALIDATION_ERRORwithdetails[].path = ["apiKey"]; unknown company:path = ["companyId"]; ExoFattura unreachable:503 SERVICE_UNAVAILABLE.
The first connection starts a full import straight away. Changing company or environment replaces the contacts imported so far with those of the new company.
How customers are imported
| ExoFattura | Company directory contact |
|---|---|
business_name (businesses) / first_name + last_name (individuals) | name |
phone | number labelled work (businesses) or home (individuals) |
mobile_phone | number labelled mobile |
email (business email, never the PEC) | email |
| — | Cliente tag, source: "exofattura", company: null |
Numbers are normalised to international format (+39…); invalid ones are ignored and customers with no valid number are not imported. No tax data (VAT number, tax code, PEC) enters the directory.
Contacts added by hand
Contacts created by hand, from CSV, vCard or the API are never changed, linked or deleted by the integration, even when they share a number with a customer: ExoFattura customers are separate contacts with source: "exofattura". Likewise a CSV / vCard import never updates a contact imported from ExoFattura.
Imported contacts
Name, company, email and numbers of an exofattura contact are managed by the sync: a PATCH /v1/contacts/:id that changes them returns 400 VALIDATION_ERROR (unchanged values are accepted). vip, blocked, tags and notes stay editable and are kept on every update. An imported contact deleted by hand comes back on the next run as long as the customer exists in ExoFattura.
Updates
- Real time: ExoFattura sends the
customer.created,customer.updatedandcustomer.deletedevents. On connection ExoVoIP registers the webhook in ExoFattura by itself (when the customer events are listed byGET /api/v1/webhooks/events); alternatively, register theintegration.webhook.urladdress by hand and save the signing secret withPUT /v1/integrations/exofattura(webhookSecret). With webhook and secret in placeintegration.modeisrealtime. - Every 15 minutes: incremental read of the changed customers only (
updated_sincefrom the highestupdated_atreceived), always on as a safety net. - Nightly reconciliation: once a day (at night, in any case within 36 hours) a full read removes the contacts of customers no longer present, catching any missed deletions.
- Sync now:
POST /v1/integrations/exofattura/syncqueues a full read immediately.
# Aggiornamento completo subito; confirmDeletions applica le eliminazioni sospese dalla soglia
curl -s -X POST https://centralino.exovoip.it/api/v1/integrations/exofattura/sync \
-H "X-API-Key: $EXOPBX_API_KEY" -H "Content-Type: application/json" \
-d '{"confirmDeletions":false}'
# Esiti degli ultimi aggiornamenti
curl -s "https://centralino.exovoip.it/api/v1/integrations/exofattura/log?limit=10" -H "X-API-Key: $EXOPBX_API_KEY"await pbx.exofattura.sync({ confirmDeletions: false });
const { items } = await pbx.exofattura.log({ limit: 10 });
for (const run of items)
console.log(run.startedAt, run.status, run.created, run.updated, run.deleted);$pbx->exofattura->sync(['confirmDeletions' => false]);
foreach ($pbx->exofattura->log(10)['items'] as $run) {
echo $run['startedAt'], ' ', $run['status'], PHP_EOL;
}pbx.exofattura.sync({"confirmDeletions": False})
for run in pbx.exofattura.log(limit=10)["items"]:
print(run["startedAt"], run["status"], run["created"], run["updated"], run["deleted"])Safety threshold
After a full read, if the contacts to delete are more than half of those imported (or ExoFattura returns an empty list) the deletions are not applied: the run is blocked, integration.pendingDeletions tells how many and lastError explains why. To apply them: POST /v1/integrations/exofattura/sync with {"confirmDeletions": true}.
Status and log
GET /v1/integrations/exofattura returns connected and integration (environment, company, keyHint, status, mode, webhook, last and next update, statistics). status:
| Value | Meaning |
|---|---|
connected | updates running |
error | last update failed (ExoFattura unreachable): retried automatically |
reconnect | key revoked or no longer valid: updates stopped until a new key is pasted |
GET /v1/integrations/exofattura/log lists the outcome of the latest runs (trigger: connect, schedule, manual, webhook; counters of customers read and contacts created, updated, deleted). POST /v1/integrations/exofattura/test checks the saved key and company.
Incoming webhook
ExoFattura calls POST /v1/integrations/exofattura/webhook/:token (public, the company's random token in the address):
X-Exobyte-Signature: sha256=<hex>signature: HMAC-SHA256 of the raw body with the endpoint secret, checked in constant time (401when missing or invalid);- deduplication on
event_id(the same event received several times is processed once); - the customer in
payload.datais used directly; only if it is missing isGET /api/v1/customers/{id}called; - out-of-order events are sorted by
occurred_at/updated_at: an update older than the one already applied, or than a deletion, is ignored; webhook.testreturns200; the response is immediate, the work is queued.
Disconnecting
DELETE /v1/integrations/exofattura stops updates and removes the webhook registered in ExoFattura. With ?deleteContacts=true it also deletes the imported contacts; without it, they stay in the directory as regular, editable contacts.
Endpoints
| Request | Scope | SDK method |
|---|---|---|
POST /v1/integrations/exofattura/webhook/:token | — | exofattura.webhook() |
GET /v1/integrations/exofattura | contacts:read | exofattura.get() |
PUT /v1/integrations/exofattura | integrations:write | exofattura.connect() |
DELETE /v1/integrations/exofattura | integrations:write | exofattura.disconnect() |
POST /v1/integrations/exofattura/companies | integrations:write | exofattura.companies() |
POST /v1/integrations/exofattura/test | integrations:write | exofattura.test() |
POST /v1/integrations/exofattura/sync | integrations:write | exofattura.sync() |
GET /v1/integrations/exofattura/log | contacts:read | exofattura.log() |