Gareth Wilson Gareth Wilson

Guide to Palmetto Finance Webhooks: Features and Best Practices

Published


Palmetto Finance webhooks keep your installer or sales platform in step with LightReach, Palmetto's residential solar and energy financing product. Instead of polling for every account, credit application and contract, you subscribe once and Palmetto posts each change to your endpoint, so you can update your CRM, move a project to the next stage or prompt a homeowner the moment something happens.

This guide covers how registrations and subscriptions work, the events and payloads Palmetto sends, how to authenticate requests with the server API key, how retries and auto-disable work, and the practices that keep a Palmetto receiver reliable.

What are Palmetto Finance webhooks?

Palmetto Finance webhooks are set up in two steps:

  1. A registration belongs to your organisation and holds the host URL and the authentication method. Events are scoped to your organisation, unless you set includeVisibleOrgEvents to true, in which case you also receive events for organisations yours has been granted access to.
  2. A subscription on that registration adds an endpoint path, an HTTP method and the event types to send. Palmetto delivers to the registration's hostUrl plus the subscription's endpointUrl.

Palmetto doesn't sign webhook requests. It offers three static ways to authenticate them:

  • Server API key, the default. Palmetto generates a key and returns it once, when you create the registration, then sends it in a header with every event.
  • Client headers. You supply a client ID and secret, and Palmetto sends them as two headers, clientId and clientSecret.
  • Client basic authorization header. You supply a client ID and secret, and Palmetto sends Authorization: Basic with the base64 of {clientId}:{clientSecret}.

The docs disagree on the API key header's name. The registration section says calls are made "with an api_key header", and the Authentication section says "an apiKey header". Check a real request before you hard-code either.

Palmetto Finance webhook features

FeatureDetails
ConfigurationRegistrations and subscriptions through the API, or Developer Settings in your organisation settings
Delivery URLRegistration hostUrl (domain only) plus subscription endpointUrl (path)
MethodSet per subscription with httpMethod. Palmetto's example uses POST
Event fieldevent in the body
SubscriptionsIndividual event types, or allEvents, allContractEvents, allStipulationEvents and allConsumerTaskEvents
AuthenticationServer API key (default), client headers, or client Basic. No signature
Timeout30 seconds per attempt
Retries5xx, 429 and network errors, up to 9 retries with exponential backoff from 3 minutes
Not retriedOther 4xx (including 401), invalid URLs, and codes you list in nonretryableErrorCodes
Auto-disableAfter 10 failures. Later events are logged as skipped and can be replayed after re-enabling
Source IPsNot published

Common events

AreaeventFires when
AccountsaccountCreated, accountUpdated, sendInviteAn account is created or updated, or a consumer application invite is sent
ApplicationsapplicationStatusA credit application's status changes
ContractscontractSending, contractSent, contractSigned, contractApproved, contractVoided, contractReinstatedA contract starts sending, is sent, signed, approved, voided or reinstated
QuotesquoteCreated, quoteVoided, activeQuoteExceedsMonthlyPaymentCapsA quote is created or voided, or an account's quote exceeds the monthly payment cap
DocumentsdocumentUploaded, documentStatusA document is uploaded, or its status changes
MilestonesmilestoneAchieved, milestoneStatusChanged, milestonePackageA milestone is reached, a milestone status changes, or a milestone package event occurs
Stipulations and requirementsstipulationAdded, stipulationCleared, allStipulationsCleared, requirementStatusChanged, requirementCompletedA stipulation is added or cleared, all are cleared, or an account requirement changes or completes
ConsumertermsAndConditionsAccepted, illinoisShineDisclosureSent, illinoisShineDisclosureSignedTerms are accepted, or the Illinois Shines disclosure is sent or signed

Bodies are flat JSON objects: event plus the fields for that event. Here is the documented shape of contractSigned, with Palmetto's placeholder values:

{
  "event": "contractSigned",
  "accountId": "palmetto-finance-account-id",
  "accountReference": "account-external-reference",
  "contractReference": "externalReference-provided-in-contract-send",
  "quoteReference": "quote-external-reference"
}

accountReference and the other *Reference fields are your own external references, which makes it easy to match an event to a record in your system. Larger events, such as applicationStatus, include applicant details, so treat bodies as personal data.

Setting up Palmetto Finance webhooks

Create a registration with your domain as hostUrl:

const response = await fetch(`${envBaseUrl}/api/webhooks/registrations`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' /* plus your API auth */ },
  body: JSON.stringify({
    name: 'Production receiver',
    hostUrl: 'https://example.com',
    includeVisibleOrgEvents: false,
  }),
});
const { webhookRegistrationId, apiKey } = await response.json();

Store apiKey straight away, for example as PALMETTO_WEBHOOK_API_KEY. Palmetto returns it only once. Then subscribe:

await fetch(`${envBaseUrl}/api/webhooks/registrations/${webhookRegistrationId}/subscriptions`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' /* plus your API auth */ },
  body: JSON.stringify({
    endpointUrl: '/webhooks/palmetto',
    httpMethod: 'POST',
    eventType: ['allEvents'],
  }),
});

Palmetto recommends allEvents as a starting point. You can also create a registration in the Palmetto Finance UI from the Developer Settings tab of your organisation settings.

With Hookdeck. Use the default server API key authentication. Palmetto shows the key only once, when you register the webhook, so copy it then. Create a Hookdeck source with the Palmetto Finance type, enter apiKey in API Key Header unless your requests show api_key, and paste the key into API Key. Hookdeck looks the header name up case-insensitively, but apiKey and api_key are different names, so if verification fails, check the stored request for the header that actually arrived. Set the registration's hostUrl to the source URL's origin and the subscription's endpointUrl to its path. Hookdeck pre-selects POST but doesn't manage the method. Palmetto's docs only show POST for httpMethod, so if you set a different method on a subscription, allow it on the source as well.

Securing Palmetto Finance webhooks

Accept the API key under either documented header name, compare it in constant time, and only then parse the body.

const crypto = require('crypto');
const express = require('express');

const API_KEY = process.env.PALMETTO_WEBHOOK_API_KEY;

function safeEqual(received, expected) {
  const a = Buffer.from(received, 'utf8');
  const b = Buffer.from(expected, 'utf8');
  // timingSafeEqual throws on unequal lengths, so check first.
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

const app = express();

app.post('/webhooks/palmetto', express.raw({ type: '*/*' }), (req, res) => {
  // The docs name the header both apiKey and api_key; Node lowercases header names.
  const received = req.get('apikey') ?? req.get('api_key');
  if (!API_KEY || typeof received !== 'string' || !safeEqual(received, API_KEY)) {
    return res.sendStatus(401);
  }

  let event;
  try {
    event = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }

  // Route on event.event, queue the work, then respond within 30 seconds.
  res.sendStatus(200);
});

The same check in Python:

import hmac
import os

API_KEY = os.environ.get("PALMETTO_WEBHOOK_API_KEY", "")


def verify_palmetto_request(headers) -> bool:
    if not API_KEY:
        return False  # fail closed when unconfigured
    # Header maps in Starlette and Flask are case-insensitive.
    received = headers.get("apiKey") or headers.get("api_key")
    if not received:
        return False
    # Compare bytes: compare_digest raises TypeError on a non-ASCII str.
    return hmac.compare_digest(received.encode("utf-8"), API_KEY.encode("utf-8"))

Once you've seen which header Palmetto sends, you can drop the other lookup.

A static key authenticates the sender, not the body. It can't detect a modified payload, and with no signed timestamp a captured request can be replayed and still pass. Use HTTPS for hostUrl and keep the key out of logs.

Make Palmetto Finance webhooks production-ready. Hookdeck Event Gateway checks the API key on every event, answers Palmetto straight away, and delivers each event to your service with retries you control.

Palmetto Finance webhook limitations and pain points

A 401 is never retried and counts towards auto-disable

The Problem: A misconfigured key loses events and, before long, switches the subscription off.

Why It Happens: Palmetto treats 4xx responses other than 429 as non-retryable, so an event your receiver rejects with 401 is marked failed at once. A subscription that accumulates 10 failures is disabled, and later events are logged as skipped.

Workarounds:

  • Test the key against a real request before you go live, and alert on any 401 from your receiver.
  • After fixing a problem, re-enable the subscription in the UI or API and replay the skipped events.

How Hookdeck Can Help: Hookdeck's Palmetto Finance source compares the header you name with the exact value you enter. Hookdeck answers Palmetto with a 200 either way, then records a request without the right key as failed verification, so it never reaches your destination. A wrong key shows up in Hookdeck's request log instead of counting against Palmetto's failure limit.

A key you only see once

The Problem: You lose the API key and have nothing to compare requests against.

Why It Happens: The server API key "is returned from the registration endpoint, and only once".

Workarounds:

  • Write the key to your secrets manager in the same code that creates the registration.
  • The docs describe no way to retrieve the key again, so plan on creating a new registration if it's lost.

How Hookdeck Can Help: Pointing the registration at a Hookdeck source means the key needs updating in one place when you replace it.

Auto-disable during outages

The Problem: A long outage on your side can leave the subscription disabled, so new events stop as well as the failed ones.

Why It Happens: Palmetto retries 5xx, 429 and network errors up to 9 times with exponential backoff starting at 3 minutes, and a subscription that accumulates 10 failures is disabled.

Workarounds:

  • Watch the subscription's status and re-enable it, then replay skipped events.
  • Answer with a 2xx quickly and process in the background, so slow work doesn't time out at 30 seconds.

How Hookdeck Can Help: Hookdeck accepts each event as it arrives, so Palmetto sees successful deliveries even when your service is down. Hookdeck then delivers to your service with retries on your schedule and lets you replay any stored event.

Best practices

Start with allEvents, route on event

Palmetto recommends allEvents. Switch on event in your handler and acknowledge anything you don't recognise with a 200, so new event types never count as failures.

Match on your own references

Events echo your external references, such as accountReference, contractReference and quoteReference. Set them on the records you create so you can match an event to your own data without a lookup.

Re-read state for important actions

Webhook bodies are summaries. Before funding or contract decisions, fetch the account or contract from the API.

Make handlers idempotent

Retries can deliver an event more than once, and no event ID is documented. Build a key from event, the ID fields and the status or milestone the event reports. See our guide to webhook idempotency.

Acknowledge fast

Answer within the 30-second timeout, ideally in well under a second, and do the work afterwards. See why to process webhooks asynchronously.

Conclusion

Palmetto Finance delivers flat JSON events for accounts, applications, contracts, quotes, documents and milestones to the URL formed by your registration and subscription, authenticated by a static API key rather than a signature. Store the key the moment it's issued, check it under the header your requests actually use, route on event, and avoid 4xx responses for anything you want retried, because they aren't retried and ten failures disable the subscription.

Hookdeck Event Gateway sits in front of your receiver, checks the key, answers Palmetto at once, and keeps every event until your service has processed it.

Get started with Hookdeck for free and handle Palmetto Finance webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.