# Palmetto Finance Webhooks

Palmetto Finance (LightReach solar financing) sends a webhook to your server when something
changes on an account: an application status moves, a contract is signed, a milestone is
achieved, a quote is created, a document is uploaded. Webhooks are **not signed**. With the
default **server API key** authentication, every request carries the API key Palmetto returned
when you registered the webhook in a header, and your endpoint authenticates the request by
comparing that header with the key you stored.

## When to Use This Skill

* How do I receive Palmetto Finance webhooks?
* How do I verify the `apiKey` header on a Palmetto Finance webhook?
* Does Palmetto Finance sign webhook payloads? (No: shared API key header)
* How do I handle `accountCreated`, `contractSigned` or `milestoneAchieved` events?
* Why does my Palmetto Finance webhook endpoint return 401, and why does Palmetto not retry it?

## Verification (core)

```javascript
const crypto = require('crypto');

// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
  const ha = crypto.createHash('sha256').update(String(a)).digest();
  const hb = crypto.createHash('sha256').update(String(b)).digest();
  return crypto.timingSafeEqual(ha, hb);
}

function verifyPalmettoFinanceRequest(
  headers,                                                          // Node lowercases names
  secret = process.env.PALMETTO_FINANCE_WEBHOOK_SECRET,
  headerName = process.env.PALMETTO_FINANCE_WEBHOOK_HEADER || 'apiKey'
) {
  if (!secret) return false;                                        // never accept when unconfigured
  const received = headers[headerName.toLowerCase()];
  return Boolean(received) && safeEqual(received, secret);          // WHOLE value, no 'Bearer ' parsing
}

```

* Header: `apiKey: <server API key>` by default. Some Palmetto pages call it `api_key`, so the
  header name is configurable (`PALMETTO_FINANCE_WEBHOOK_HEADER`). Check a real delivery.
* Compare the **whole** header value, case-insensitive on the name only. Do not strip or parse a
  `Bearer ` prefix. This is what Hookdeck's Palmetto Finance source does.
* Check the credential **first, for every HTTP method**, then handle an empty body.
* Return `401` on a missing or wrong key, `400` for a non-JSON body, `200` once handled.
* Read the body raw (`express.raw({ type: '*/*' })`) and authenticate before parsing it.
* **A static shared credential authenticates the sender, not the body, and gives no replay
  protection** (no signature, no timestamp). Use HTTPS and rotate the key if it leaks.
* Only the server API key mechanism is covered. Palmetto's **client headers** (`clientId` and
  `clientSecret`) and **client basic authorization** options are not covered by Hookdeck's
  Palmetto Finance source, and the examples don't check them.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/palmetto-finance-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/palmetto-finance-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/palmetto-finance-webhooks/examples/fastapi/).

## HTTP Methods

Each webhook subscription sets its own `httpMethod` (the docs example uses `POST`), so the
examples accept `GET`, `POST`, `PUT`, `PATCH` and `DELETE` on the route and handle an empty body.
Hookdeck's Palmetto Finance source pre-selects `POST` without managing the method, so any other
method your subscription uses must also be allowed on the source.

## Event Types

The body is a flat JSON object: an `event` field plus the event's own fields (`accountId`,
`applicationId`, ...). Dispatch on `event`.

| Event | Sent When |
| --- | --- |
| `accountCreated` | A new account is created |
| `accountUpdated` | An account is updated |
| `sendInvite` | A consumer application invite is sent |
| `applicationStatus` | A credit application's status changes |
| `contractApproved` | A signed contract is approved by Palmetto Finance |
| `contractSending` | An async contract send has begun |
| `contractSent` | A contract is sent to the consumer |
| `contractSigned` | The consumer signs a contract |
| `contractVoided` | A contract is voided |
| `contractReinstated` | A voided contract is reinstated |
| `documentStatus` | An uploaded document's status changes |
| `documentUploaded` | A new document is uploaded |
| `milestonePackage` | Milestone package events |
| `milestoneAchieved` | A milestone is reached |
| `milestoneStatusChanged` | A milestone's status changes |
| `quoteCreated` | A quote is created |
| `quoteVoided` | A quote is voided |
| `requirementStatusChanged` | An account requirement's status changes |
| `requirementCompleted` | An account requirement is completed |
| `termsAndConditionsAccepted` | Terms and conditions are accepted |
| `allConsumerTaskEvents` | A consumer task is created, updated or outstanding (`status`, `activityType`) |

Palmetto's docs list more subscription names (for example `stipulationAdded`,
`stipulationCleared` and `allStipulationsCleared`); see
[references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/palmetto-finance-webhooks/references/overview.md).

## Retries

Palmetto waits up to 30 seconds per attempt. It retries `5xx`, `429` and network errors up to 9
times (10 attempts) with exponential backoff from 3 minutes. Other `4xx` responses, including
`401`, are not retried. 10 failures disable the subscription.

## Environment Variables

```bash
PALMETTO_FINANCE_WEBHOOK_HEADER=apiKey            # or api_key, whatever your requests show
PALMETTO_FINANCE_WEBHOOK_SECRET=your-server-api-key   # exact, whole header value

```

## Setup Note

Use the default server API key authentication. Palmetto shows the key only once, when you register the webhook. Enter apiKey as the header unless your requests show api_key.

In Hookdeck, create a source with type **Palmetto Finance**, enter that header name and the key,
and leave the method as the pre-selected `POST` unless your subscription uses another method.

## Local Development

Palmetto Finance can't reach `localhost`. Tunnel with the Hookdeck CLI (no account required):

```bash
npx hookdeck-cli listen 3000 palmetto-finance --path /webhooks/palmetto-finance

```

Use the printed URL as the registration `hostUrl` (and the subscription `endpointUrl` for the
path).

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/palmetto-finance-webhooks/references/overview.md) - What Palmetto Finance webhooks are, events and payload shape
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/palmetto-finance-webhooks/references/setup.md) - Register a webhook, subscribe to events, store the key
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/palmetto-finance-webhooks/references/verification.md) - Header check details, gotchas, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/palmetto-finance-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests