# Guide to Aurora Solar Webhooks: Features and Best Practices

Aurora Solar webhooks tell your systems what is happening in your Aurora tenant: a project is created or sold, a design is ready, an AutoDesigner or performance simulation job finishes, an agreement is signed. If you sell or design solar installations on Aurora Solar, webhooks keep your CRM and post-sales workflows in step with Aurora without polling its API for status changes.

This guide covers how Aurora Solar's URL-template webhooks work, the events you can subscribe to, how to authenticate requests that Aurora doesn't sign, and the practices that keep a receiver reliable.

## What are Aurora Solar webhooks?

Aurora Solar webhooks look different from most. There is no JSON body. Aurora says its webhooks "are fired as GET requests to a URL generated using a template provided by you", and the data travels in that URL.

A webhook subscription names one event and a URL template. The template contains placeholders for the event's attributes, and Aurora fills them in when the event fires. This template:

```
https://www.yourapp.com/auto_designer_job_completed?design_id=<DESIGN_ID>&job_id=<JOB_ID>&status=<STATUS>

```

produces requests like this:

```
GET https://www.yourapp.com/auto_designer_job_completed?design_id=3fa85f64-5717-4562-b3fc-2c963f66afa6&job_id=5cf99a21-5717-4562-b3fc-2c963f66abc6&status=succeeded

```

Aurora deliberately sends only "proprietary identifiers and statuses". To get the project, design or job itself, call the Aurora API with the IDs from the query string.

Requests are not signed. Aurora offers two optional ways to authenticate them: a header token, where you choose a header name and value, or Basic Auth.

## Aurora Solar webhook features

| Feature | Details |
| --- | --- |
| Configuration | The webhooks dashboard, or `POST /tenants/{tenant_id}/webhooks` |
| Subscription model | One event per subscription, at most 5 subscriptions per event per tenant |
| Method | `GET`, with no body |
| Payload | Query parameters from your URL template: identifiers and statuses only |
| Event name | Not sent, unless your template adds it as a static parameter |
| Filters | Allowed values per URL attribute, such as `STATUS` of `succeeded` |
| Authentication | Optional: header token (`header_auth_key`, `header_auth_value`) or Basic Auth |
| Signature | None |
| Response window | 10 seconds |
| Retries | 5 more attempts over about 24 hours, on a timeout or a `3xx`, `4xx` or `5xx` |
| Auto-disable | When all retries fail within 48 hours, with at least 50 attempts |
| Ordering | Not guaranteed |
| Source IPs | Not published. Aurora suggests a DNS allowlist on `*.aurorasolar.com` |

## Common events

Aurora documents 32 events. Each one lists the URL attributes you can put in the template and the filters it supports.

| Area | Events | Typical attributes |
| --- | --- | --- |
| Projects | `project_created`, `project_changed`, `project_status_changed`, `project_deleted` | `<PROJECT_ID>`, `<SOURCE>`, `<STATUS>`, `<ATTRIBUTES>` |
| Designs | `design_created`, `design_deleted`, `asset_created` | `<PROJECT_ID>`, `<DESIGN_ID>`, `<ASSET_TYPE>` |
| Design requests | `design_request_created`, `design_request_completed`, `design_request_rejected` | `<DESIGN_REQUEST_ID>`, `<STATUS>` |
| Jobs | `auto_designer_job_completed`, `performance_simulation_job_completed`, `ai_roof_job_completed`, `proposal_pdf_generation_job_completed` and others | `<DESIGN_ID>`, `<JOB_ID>`, `<STATUS>`, `<SOURCE>` |
| Sales | `milestone_created`, `milestone_shared`, `agreement_status_changed`, `web_proposal_url_created`, `web_proposal_viewed` | `<STAGE>`, `<AGREEMENT_ID>`, `<IS_FIRST_VIEW>` |
| Other | `imagery_lidar_completed`, `ahj_lookup_completed`, `order_created`, `site_survey_requested`, `utility_bill_upload_job_completed` and more | Varies by event |

A few details catch people out:

* Several job events, including `ai_roof_job_completed`, `proposal_pdf_generation_job_completed` and `fire_pathway_placement_job_completed`, fire only for jobs started through the API, not from the Aurora app.
* `project_changed` fires only when one of a listed set of attributes changes, such as `status`, `owner_id` or `customer_email`, and `<ATTRIBUTES>` is a comma-separated list of what changed.
* A project status "can be any string", unlike the fixed statuses of other events.
* On `milestone_created`, `<CHANGE_TYPES>` is empty unless `<STAGE>` is `change_order`. Aurora warns that a `CHANGE_TYPES` filter without a `STAGE=change_order` filter stops every other milestone from arriving.
* Order IDs are 24-character hex ObjectIds, not UUIDs.

## Setting up Aurora Solar webhooks

Because each request carries only what your template asks for, design the template first. Add a static parameter that names the event, because nothing else in the request does. Aurora's own example uses `?aurora_event=design_request_rejected`.

1. Pick a header name and a long random value, for example `X-Aurora-Webhook-Token` and the output of `openssl rand -hex 32`. Store the value server-side.
2. Create a subscription in the Aurora webhooks dashboard, or through the API, with the event, an HTTPS URL template and the header token:

```
POST /tenants/{tenant_id}/webhooks

{
  "webhook": {
    "description": "Project created",
    "event": "project_created",
    "url_template": "https://example.com/webhooks/aurora-solar?aurora_event=project_created&project_id=<PROJECT_ID>&source=<SOURCE>",
    "enabled": true,
    "header_auth_key": "X-Aurora-Webhook-Token",
    "header_auth_value": "your-long-random-value"
  }
}

```

1. Optionally add filters so Aurora only fires for the values you care about, such as `succeeded` jobs.
2. Repeat for each event you need.

**With Hookdeck.** Aurora Solar is a supported source in Hookdeck. Create a source with the Aurora Solar type, enter the header name as the **API Key Header** and the header value as the **API Key**, and use the Hookdeck source URL as the start of your URL template, followed by your query parameters. Hookdeck's setup note says to set `header_auth_key` and `header_auth_value` on the Aurora webhook and enter them on the source, because webhooks without them send nothing to verify. Basic Auth subscriptions are not covered by this source type, so use the header token. The source pre-selects `GET`, since that is the method Aurora uses. The method isn't managed, so keep `GET` in the source's allowed methods. A request without the right header still gets a `200` from Hookdeck, but Hookdeck marks it as failed verification and doesn't deliver it to your service.

For local development, `hookdeck listen 3000 aurora-solar --path /webhooks/aurora-solar` from the [Hookdeck CLI](/docs/cli) gives you a public HTTPS URL that forwards to your local server.

## Securing Aurora Solar webhooks

Check the header before you read the query string. Look the header up case-insensitively, compare the whole value in constant time, and check lengths before calling `crypto.timingSafeEqual`, which throws on buffers of different lengths.

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

function safeEqual(a, b) {
  const x = Buffer.from(String(a), 'utf8');
  const y = Buffer.from(String(b), 'utf8');
  return x.length === y.length && crypto.timingSafeEqual(x, y);
}

function verifyAuroraRequest(headers) {
  const secret = process.env.AURORA_SOLAR_WEBHOOK_SECRET; // your header_auth_value
  if (!secret) return false; // fail closed when unconfigured
  const name = (process.env.AURORA_SOLAR_WEBHOOK_HEADER || 'X-Aurora-Webhook-Token').toLowerCase();
  const received = headers[name];
  return typeof received === 'string' && safeEqual(received, secret);
}

const app = express();

// Aurora sends GET with no body. express.raw() still reads any body a proxy adds.
app.get('/webhooks/aurora-solar', express.raw({ type: '*/*' }), (req, res) => {
  if (!verifyAuroraRequest(req.headers)) {
    return res.sendStatus(401);
  }

  const { aurora_event, project_id, design_id, job_id, status } = req.query;
  if (typeof aurora_event !== 'string') return res.sendStatus(400);

  // Queue the IDs, then fetch the full objects from the Aurora API asynchronously.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import hmac
import os

def verify_aurora_request(headers) -> bool:
    secret = os.environ.get("AURORA_SOLAR_WEBHOOK_SECRET")
    if not secret:
        return False
    name = os.environ.get("AURORA_SOLAR_WEBHOOK_HEADER", "X-Aurora-Webhook-Token")
    received = headers.get(name)  # case-insensitive in Starlette and Flask
    if not received:
        return False
    return hmac.compare_digest(received.encode("utf-8"), secret.encode("utf-8"))

```

A static header proves the sender knows the value, but it doesn't prove the query string is unchanged, and it gives no replay protection: nothing is signed and Aurora sends no timestamp. Use an HTTPS URL, so the query string and the header are encrypted in transit, keep the value long and random, and keep it out of logs. Because the payload is only IDs and statuses, re-reading the object from the Aurora API before acting also limits what a forged request can do.

> **Make Aurora Solar webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) checks the header token on every Aurora request, stores each one with its query string, and retries delivery to your service, so a long outage doesn't get your subscription auto-disabled.

## Aurora Solar webhook limitations and pain points

### The request doesn't say which event it is

**The Problem:** A handler that receives several Aurora events on one URL can't tell them apart.

**Why It Happens:** Each subscription is for one event, and Aurora sends only the attributes your template includes. There is no event header and no body.

**Workarounds:**

* Put a static parameter such as `aurora_event=project_created` in every template.
* Or give each event its own path, such as `/webhooks/aurora-solar/project_created`.

**How Hookdeck Can Help:** Hookdeck [filters](/docs/filters) can match on the query string, so one Aurora Solar source can route `aurora_event=project_created` to your CRM sync and job events to your design pipeline.

### No signature, and the credential is optional

**The Problem:** A subscription created without a header token sends requests that anyone could imitate.

**Why It Happens:** Aurora's authentication is optional, and both of its options are static credentials.

**Workarounds:**

* Always set `header_auth_key` and `header_auth_value`.
* Treat each webhook as a prompt to read the object from the Aurora API, not as the source of truth.

**How Hookdeck Can Help:** Hookdeck's Aurora Solar source compares the header you name against the exact value you enter, and logs every request with its headers and query string.

### A failing endpoint gets switched off

**The Problem:** After a long enough outage, Aurora stops sending a subscription's events until someone re-enables it.

**Why It Happens:** Aurora retries a failed webhook 5 more times, at exponentially increasing intervals over 24 hours: Aurora's table lists approximate intervals of 30 seconds, 5 minutes, 30 minutes, 3 hours and 20 hours. If all retries for a subscription fail within 48 hours, and it has had at least 50 attempts, Aurora disables it.

**Workarounds:**

* Return `200` within 10 seconds and do slow work afterwards.
* Re-enable the subscription in the dashboard or through the Webhooks API once the failure is fixed.
* For events you missed entirely, Aurora can resend notifications within 30 days of the original event through your account representative.

**How Hookdeck Can Help:** Hookdeck accepts each request as it arrives and stores it, then delivers to your service with [automatic retries](/docs/retries) on a schedule you control. Your own downtime doesn't count against Aurora's limit, and [Issues](/docs/issues) alert you when deliveries fail.

### Out-of-order and repeated webhooks

**The Problem:** An agreement shows as `viewed` after it was `signed`, or the same job completion arrives twice.

**Why It Happens:** Aurora says ordering "is not guaranteed", and that it may occasionally send the same webhook twice.

**Workarounds:**

* Fetch the current state from the API rather than applying statuses in arrival order.
* Deduplicate on identifiers in the query string, such as `job_id` or `design_request_id`, as Aurora suggests.

**How Hookdeck Can Help:** Hookdeck [deduplication](/docs/deduplication) can drop repeated requests before they reach your handler.

## Best practices

### Template only what you need

Each attribute in the template is another query parameter to parse and log. Include the IDs you need to call the API, the status if you filter on it, and the static event name.

### Use filters to cut noise

Filters run in Aurora, before anything is sent. A `performance_simulation_job_completed` subscription filtered to `STATUS` of `succeeded` only fires for jobs worth acting on.

### Rotate the header value carefully

Aurora says each retry "will use the most recent webhook definition so that errors in URLs or credentials can be corrected". After you change `header_auth_value`, even retries of older events carry the new value, so update your receiver at the same time.

### Acknowledge fast and process asynchronously

Check the header, queue the IDs, return `200`, then call the Aurora API. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Make handlers idempotent

Key on the event name and the IDs in the query string, and update rather than insert. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

## Conclusion

Aurora Solar sends each webhook as an unsigned `GET` whose query string holds the identifiers and statuses your URL template asks for. Add a static event name to every template, set a header token and compare it in constant time over HTTPS, fetch full objects from the API, and answer within 10 seconds so a subscription is never auto-disabled.

[Hookdeck Event Gateway](https://hookdeck.com) accepts Aurora's `GET` requests, checks the header token, routes on the query string and holds each event until your service has processed it.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Aurora Solar webhooks reliably in minutes.