Guide to Palmetto Finance Webhooks: Features and Best Practices
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:
- A registration belongs to your organisation and holds the host URL and the authentication method. Events are scoped to your organisation, unless you set
includeVisibleOrgEventstotrue, in which case you also receive events for organisations yours has been granted access to. - A subscription on that registration adds an endpoint path, an HTTP method and the event types to send. Palmetto delivers to the registration's
hostUrlplus the subscription'sendpointUrl.
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,
clientIdandclientSecret. - Client basic authorization header. You supply a client ID and secret, and Palmetto sends
Authorization: Basicwith 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
| Feature | Details |
|---|---|
| Configuration | Registrations and subscriptions through the API, or Developer Settings in your organisation settings |
| Delivery URL | Registration hostUrl (domain only) plus subscription endpointUrl (path) |
| Method | Set per subscription with httpMethod. Palmetto's example uses POST |
| Event field | event in the body |
| Subscriptions | Individual event types, or allEvents, allContractEvents, allStipulationEvents and allConsumerTaskEvents |
| Authentication | Server API key (default), client headers, or client Basic. No signature |
| Timeout | 30 seconds per attempt |
| Retries | 5xx, 429 and network errors, up to 9 retries with exponential backoff from 3 minutes |
| Not retried | Other 4xx (including 401), invalid URLs, and codes you list in nonretryableErrorCodes |
| Auto-disable | After 10 failures. Later events are logged as skipped and can be replayed after re-enabling |
| Source IPs | Not published |
Common events
| Area | event | Fires when |
|---|---|---|
| Accounts | accountCreated, accountUpdated, sendInvite | An account is created or updated, or a consumer application invite is sent |
| Applications | applicationStatus | A credit application's status changes |
| Contracts | contractSending, contractSent, contractSigned, contractApproved, contractVoided, contractReinstated | A contract starts sending, is sent, signed, approved, voided or reinstated |
| Quotes | quoteCreated, quoteVoided, activeQuoteExceedsMonthlyPaymentCaps | A quote is created or voided, or an account's quote exceeds the monthly payment cap |
| Documents | documentUploaded, documentStatus | A document is uploaded, or its status changes |
| Milestones | milestoneAchieved, milestoneStatusChanged, milestonePackage | A milestone is reached, a milestone status changes, or a milestone package event occurs |
| Stipulations and requirements | stipulationAdded, stipulationCleared, allStipulationsCleared, requirementStatusChanged, requirementCompleted | A stipulation is added or cleared, all are cleared, or an account requirement changes or completes |
| Consumer | termsAndConditionsAccepted, illinoisShineDisclosureSent, illinoisShineDisclosureSigned | Terms 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
401from 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
2xxquickly 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.