These docs are a work in progress and may not be fully up to date. Some pages may contain internal notes for our team.
Skip to Content

Integration — Backend → third-party

All outbound calls the Laravel backend makes to external services. For the full data-flow context see data-flow-builder.md and storage-and-cdn.md (session 6 ★). For the customer-facing autocomplete configuration UI, see ../integrations/autocomplete-providers.md.


1. Paddle

Paddle is the merchant of record for all subscription billing. The backend talks to Paddle in two directions: inbound webhook and outbound REST.

Webhook receiver

POST /api/webhooks/paddleBillingController::webhook() (app/Http/Controllers/BillingController.php:185)

Paddle delivers a signed POST to this endpoint for every billing lifecycle event. The controller verifies the HMAC-SHA256 Paddle-Signature header (5-minute timestamp window) before processing. Duplicate events are dropped by checking the paddle_webhook_events table for the event_id.

Handled event types (BillingController.php:527–545):

GroupEvents
Subscriptionsubscription.created, subscription.trialing, subscription.activated, subscription.updated, subscription.past_due, subscription.paused, subscription.resumed, subscription.canceled
Transactiontransaction.paid, transaction.completed, transaction.payment_failed

Every processed event is written to paddle_webhook_events with event_id, event_type, occurred_at, processed_at, user_id, and a payload_summary JSON column.

Outbound calls

All outbound calls are made by BillingController::paddleRequest() (BillingController.php:607), which sets the base URL, bearer token, and Paddle-Version: 1 header.

MethodOperationTriggered by
POST /transactionsCreate checkout transactionBillingController::checkout() — user initiates subscription upgrade
GET /subscriptions/{id}Fetch subscription stateBillingController::sync() and portal()
GET /subscriptionsList subscriptions for a customerBillingController::findSubscriptionForUser()
GET /customersResolve customer by emailBillingController::resolveCustomerIdForUser()
GET /transactionsList invoices for a customerBillingController::invoices()
GET /transactions/{id}Fetch a single transactionBillingController::invoicePdf()
GET /transactions/{id}/invoiceGet invoice PDF URLBillingController::invoicePdf()

Environment variables

VariablePurpose
PADDLE_ENVIRONMENTsandbox or production (default sandbox)
PADDLE_API_KEYBearer token for Paddle REST API
PADDLE_CLIENT_TOKENPublic token used by the frontend Paddle.js checkout
PADDLE_WEBHOOK_SECRETHMAC secret for webhook signature verification
PADDLE_BRONZE_PRICE_IDPaddle price ID for the Bronze plan
PADDLE_PREMIUM_PRICE_IDPaddle price ID for the Premium plan
PADDLE_API_BASE_URLAPI base (default https://api.paddle.com)

Source: config/services.php:60–68.


2. Cloudflare R2 / S3

Disk configuration

The r2 disk is defined in config/filesystems.php:61–70. It uses the S3 driver pointed at the Cloudflare R2 S3-compatible endpoint:

driver: s3 region: auto use_path_style_endpoint: true throw: true

The s3 disk (lines 48–58) is the standard AWS S3 configuration and is available as a fallback but is not used by default. The active upload disk is selected via FILESYSTEM_DISK (default local in development; set to r2 in production).

What is stored

Object pathContentWho uploads
finders/{key}/v/{hash}.jsonImmutable versioned finder payload; CacheControl: public, max-age=31536000, immutableExternalController::syncFinderDetails() on publish
finders/{key}/config.jsonPointer file containing hash and published_at; CacheControl: public, max-age=60Same publish call
workspaces/{id}/locations/{id}/{uuid}.{ext}Location image (jpg/png/webp/gif, max 2 MB)LocationController::uploadImage() (LocationController.php:271)
avatars/{user_id}/{uuid}.{ext}User avatar (jpg/png/webp, max 1 MB)AccountSecurityController::uploadAvatar() (AccountSecurityController.php:36)

The CDN base URL is assembled from config('services.cdn.base_url') (env CDN_BASE_URL, default https://cdn.locationfinders.com). All public object URLs are constructed as CDN_BASE_URL/{key} — the R2 bucket is not exposed directly.

Finder payload publish flow

On POST /ext/finders/details/{token}/sync, ExternalController::syncFinderDetails() (ExternalController.php:191–202):

  1. Builds the full finder JSON payload.
  2. sha1-hashes the payload (excluding created and hash keys) and takes the first 12 characters as hash.
  3. Writes the versioned file at finders/{key}/v/{hash}.json (immutable, 1-year TTL).
  4. Writes the pointer file at finders/{key}/config.json (60-second TTL).

See data-flow-builder.md for the full publish sequence diagram.

Environment variables

VariablePurpose
R2_ACCESS_KEY_IDR2 API key ID
R2_SECRET_ACCESS_KEYR2 API secret
R2_BUCKETBucket name
R2_ENDPOINTR2 S3-compatible endpoint URL (e.g. https://<account>.r2.cloudflarestorage.com)
CDN_BASE_URLPublic CDN domain in front of the bucket (default https://cdn.locationfinders.com)
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_DEFAULT_REGION / AWS_BUCKET / AWS_ENDPOINTStandard S3 disk — not used in default production config

Source: config/filesystems.php and config/services.php:40–42.


3. Autocomplete providers

The backend proxies all autocomplete search requests on behalf of the widget so that BYOK keys are never exposed in the browser. The entry point is MappingController (app/Http/Controllers/MappingController.php).

Request flow

  1. Widget calls GET /api/mapping/autocomplete?input=…&finder_token=…&latitude=…&longitude=…&sessionToken=….
  2. MappingController::resolveProvider() looks up the finder by token (cached 60 seconds; only provider slug + finder ID are cached — never the plaintext key).
  3. Reads the settings.autocompleteProvider setting from the finder’s settings JSON column to get the provider slug.
  4. If a BYOK key is configured, retrieves FinderIntegration.api_key fresh on every request (decrypted, never cached). Source: MappingController.php:95–99.
  5. Calls AutocompleteProviderFactory::make($slug, $apiKey) (app/Services/Autocomplete/AutocompleteProviderFactory.php:9) to get a provider instance.
  6. Calls $provider->search(…) and returns normalized predictions.

For Google (the only provider requiring a round-trip for coordinates), the widget then calls GET /api/mapping/places?place_id=…&finder_token=… which proxies GoogleProvider::placeDetails().

Provider classes

All six providers implement AutocompleteProviderInterface (app/Services/Autocomplete/AutocompleteProviderInterface.php), which defines search() and placeDetails().

SlugClassUpstream endpointReturns coords in search()?placeDetails() needed?
googleGoogleProvidermaps.googleapis.com/maps/api/place/autocomplete/jsonNo — lat/lng are nullYes — place/details/json
mapboxMapboxProviderapi.mapbox.com/geocoding/v5/mapbox.places/{input}.jsonYesNo — returns null
hereHereProviderautocomplete.search.hereapi.com/v1/autocompleteYes (via position)No
radarRadarProviderapi.radar.io/v1/search/autocompleteYes (via latitude/longitude)No
geoapifyGeoapifyProviderapi.geoapify.com/v1/geocode/autocompleteYesNo
dropaSearch (default)DropaSearchProvidernominatim.openstreetmap.org/searchYesNo

DropaSearchProvider is the no-key fallback. It hits the public Nominatim OSM API with a User-Agent: Dropafinder/1.0 header (source: DropaSearchProvider.php:9). No API key is required.

All providers use Http::timeout(5) for the upstream call. Partial failures return an empty array — the widget receives {"predictions": []}.

BYOK key storage

Customer-supplied API keys are stored encrypted in the finder_integrations table (model: FinderIntegration, controller: FinderIntegrationController). The allowed BYOK providers are google, radar, mapbox, here, geoapifydropaSearch (Nominatim) never has a key (FinderIntegrationController.php:14).

Keys are managed via PUT /api/finders/{id}/integrations/{provider} and DELETE /api/finders/{id}/integrations/{provider}. Saving or deleting a key busts the autocomplete_finder_{token} cache entry.

The server-side GOOGLE_API_KEY (from config/services.php:43) and RADAR_API_KEY (config/services.php:47) are system-level keys used by the AI import enrichment flow (see section 4), not by the autocomplete proxy.


4. AI service (Gemini)

Provider

Google Gemini via the REST generateContent API. The model is configurable; the default is gemini-2.5-flash-lite (config/services.php:51).

Endpoint pattern: https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={GEMINI_API_KEY}

Usage

The AI service is called in two flows:

Location import (AiLocationImportController::requestGeminiDraft(), AiLocationImportController.php:463):

  • Called from GenerateAiLocationImportJob (queue: aigen).
  • Takes user-supplied answers (brand name, business type, geographic scope, etc.) and workspace custom-field definitions.
  • Returns a JSON array of locations with name, address, tags, custom fields, and optional coordinates.
  • Temperature: 0.2; responseMimeType: application/json.
  • After Gemini drafts locations, enrichDraftPayloadWithGoogle() optionally calls Google Places text search + details to enrich addresses, phone numbers, websites, opening hours, and coordinates.

Location improvement (AiLocationImprovementController::requestGeminiImprovement(), AiLocationImprovementController.php:511):

  • Called from GenerateAiLocationImprovementJob (queue: aigen).
  • Takes the full workspace location dataset (serialized to JSON) plus active map context.
  • Returns location_updates (per-location field suggestions) and group_recommendations (tag merges, new categories, splits, etc.).
  • Temperature: 0.2; responseMimeType: application/json.
  • After Gemini suggestions are normalized, geocodePayload() optionally calls Google Geocoding API to fill missing coordinates.

Both flows share identical generation config and a 90-second HTTP timeout with a 10-second connect timeout. Failures throw ValidationException to be caught by the job’s failed() handler.

Token pricing

config/services.php:52–57 stores per-million token pricing for gemini-2.5-flash-lite (default: $0.10 input / $0.40 output). The usage_metadata JSON column on ai_location_import_batches and ai_location_improvement_batches captures promptTokenCount, candidatesTokenCount, totalTokenCount, and thoughtsTokenCount from the Gemini response.

Environment variables

VariablePurpose
GEMINI_API_KEYGoogle Gemini API key
GEMINI_MODELModel name (default gemini-2.5-flash-lite)
GEMINI_2_5_FLASH_LITE_INPUT_PER_MILLIONInput token cost override (default 0.10)
GEMINI_2_5_FLASH_LITE_OUTPUT_PER_MILLIONOutput token cost override (default 0.40)
GOOGLE_API_KEYGoogle Places + Geocoding key used for AI import/improvement enrichment

Source: config/services.php:49–58 and config/services.php:43.


5. Mail

Transport

Laravel’s mailer. The default transport in .env.example is log (development). The production transport is not hardcoded — it is controlled by MAIL_MAILER. config/mail.php ships with drivers for smtp, ses, postmark, resend, sendmail, and log. Separate tokens (POSTMARK_TOKEN, RESEND_KEY) are wired in config/services.php:17–29 for Postmark and Resend respectively.

🔴 [NEEDS CLARIFICATION: Confirm which mail transport (smtp, postmark, resend, or ses) is active in the production environment.]

What triggers email

TriggerMailable / mechanismSource
Workspace member invitationInviteUserMailInviteController::sendInvite() (InviteController.php:20)

No other Mail:: calls or additional Mailable classes were found in the codebase at the time of writing. Password-reset and email-verification flows are not present in the custom AuthController — Laravel’s built-in notification system may handle these, or they may be absent.

🔴 [NEEDS CLARIFICATION: Are password-reset and email-verification emails sent? If so, which Notification class or SMTP provider handles them?]

Environment variables

VariablePurpose
MAIL_MAILERActive transport (smtp, postmark, resend, ses, log, etc.)
MAIL_HOSTSMTP host (default 127.0.0.1)
MAIL_PORTSMTP port (default 2525)
MAIL_USERNAMESMTP username
MAIL_PASSWORDSMTP password
MAIL_ENCRYPTIONTLS/SSL (tls recommended)
MAIL_FROM_ADDRESSSender address
MAIL_FROM_NAMESender display name
POSTMARK_TOKENAPI token for Postmark transport
RESEND_KEYAPI key for Resend transport

Source: config/mail.php and config/services.php:17–29.


6. ipinfo.io

ipinfo.io is not called by the Laravel backend. It is called client-side by the embedded widget (Svelte/snippet.js) to resolve the visitor’s approximate latitude/longitude for geolocation-biased autocomplete and analytics context. The call is:

fetch("https://ipinfo.io/json?token=<IPINFO_TOKEN>")

The token is embedded in the compiled widget bundle. The loc field of the response ("lat,lng") is stored in a Svelte writable store and passed to the backend autocomplete proxy as the optional latitude/longitude query parameters.

⚠️ Warning: The ipinfo.io token is visible in the public JavaScript bundle. It should be rate-limited to the production domain and scoped to the minimum required API access.

The backend has no IPINFO_* env vars and no ipinfo references in any PHP source file. If a server-side geolocation lookup is ever needed, the token and call should be moved to a dedicated backend endpoint.

See data-flow-end-user.md for the full widget initialization sequence including the ipinfo.io phase.


Summary table

ServiceDirectionTriggerKey class(es)
PaddleInbound webhook + outbound RESTSubscription lifecycle events; user-initiated checkoutBillingController
Cloudflare R2Outbound uploadFinder publish, location image upload, avatar uploadExternalController, LocationController, AccountSecurityController
Google Places / GeocodingOutbound proxy (autocomplete) + outbound enrichment (AI import)Widget autocomplete + AI import/improvement geocodingGoogleProvider, AiLocationImportController, AiLocationImprovementController
MapboxOutbound proxyWidget autocomplete (BYOK)MapboxProvider
HEREOutbound proxyWidget autocomplete (BYOK)HereProvider
RadarOutbound proxyWidget autocomplete (BYOK)RadarProvider
GeoapifyOutbound proxyWidget autocomplete (BYOK)GeoapifyProvider
DropASearch (Nominatim)Outbound proxyWidget autocomplete (default / no key)DropaSearchProvider
Google GeminiOutbound RESTAI location import generation, AI location improvement generationAiLocationImportController, AiLocationImprovementController
Mail transportOutboundWorkspace member inviteInviteController + InviteUserMail
ipinfo.ioWidget-side (not backend)Widget visitor geolocationn/a (external snippet)

See also: