ExoVoIP Developer guide

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"
  • 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 carry keyHint (last characters). Omit apiKey to change the connection while keeping the key.
  • environment: production (https://api.exofattura.it) or test (https://api-test.exofattura.it). These are the only allowed addresses: free URLs cannot be set.
  • companyId: ExoFattura company (X-Company-Id header). POST /v1/integrations/exofattura/companies returns the companies available for a key, to offer them in a menu; with available: false the id is entered by hand.
  • Invalid, revoked or wrong-environment key: 400 VALIDATION_ERROR with details[].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

ExoFatturaCompany directory contact
business_name (businesses) / first_name + last_name (individuals)name
phonenumber labelled work (businesses) or home (individuals)
mobile_phonenumber 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.updated and customer.deleted events. On connection ExoVoIP registers the webhook in ExoFattura by itself (when the customer events are listed by GET /api/v1/webhooks/events); alternatively, register the integration.webhook.url address by hand and save the signing secret with PUT /v1/integrations/exofattura (webhookSecret). With webhook and secret in place integration.mode is realtime.
  • Every 15 minutes: incremental read of the changed customers only (updated_since from the highest updated_at received), 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/sync queues 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"

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:

ValueMeaning
connectedupdates running
errorlast update failed (ExoFattura unreachable): retried automatically
reconnectkey 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 (401 when missing or invalid);
  • deduplication on event_id (the same event received several times is processed once);
  • the customer in payload.data is used directly; only if it is missing is GET /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.test returns 200; 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

RequestScopeSDK method
POST /v1/integrations/exofattura/webhook/:token—exofattura.webhook()
GET /v1/integrations/exofatturacontacts:readexofattura.get()
PUT /v1/integrations/exofatturaintegrations:writeexofattura.connect()
DELETE /v1/integrations/exofatturaintegrations:writeexofattura.disconnect()
POST /v1/integrations/exofattura/companiesintegrations:writeexofattura.companies()
POST /v1/integrations/exofattura/testintegrations:writeexofattura.test()
POST /v1/integrations/exofattura/syncintegrations:writeexofattura.sync()
GET /v1/integrations/exofattura/logcontacts:readexofattura.log()