# Guide to Procore Webhooks: Features and Best Practices

Procore webhooks tell your systems when construction project data changes. When an RFI, submittal or another Procore record is created, updated or deleted, a hook can send a notification to your service, so you can keep an ERP, document system or reporting database in step with Procore without polling its API.

This guide covers how Procore hooks are configured, how to authenticate deliveries with the Authorization Header value, what Procore tells you about each event, and the practices that make a Procore receiver reliable.

## What are Procore webhooks?

In Procore, a webhook is called a **hook**. Company and project administrators create hooks that subscribe "to event notifications for one or more Procore API resources when Create, Update, or Delete actions occur". Procore's example is a third-party RFI integration that wants to know whenever a new RFI is created. When a subscribed event happens, Procore sends a `POST` to the hook's endpoint URL.

Each hook has a **Payload Format**, the REST API payload version Procore sends. You pick it when you create the hook, and you can't change it afterwards: to switch versions, you delete the hook and create a new one.

Procore doesn't sign webhook requests. The hook has an optional **Authorization Header** field, and Procore places that value in the `Authorization` header of every `POST`. Comparing the header with the value you set is how you tell a real Procore request from a forged one.

## Procore webhook features

| Feature | Details |
| --- | --- |
| Configuration | Company **Admin** > **Webhooks**, or Project **Admin** > **Webhooks**, then **Create Hook** |
| Permissions | Admin on the company or project Admin tool |
| Method | `POST` |
| Events | Create, Update and Delete on the Procore API resources you select |
| Payload Format | The REST API payload version. Fixed once the hook is created |
| Authentication | Optional **Authorization Header**, sent verbatim as `Authorization` |
| Signature and timestamp | None |
| Event history | A rolling 28 days per hook |
| Event statuses | Delivered, Discarded, Failed, Queued, Retrying |
| Retry schedule, timeout, source IPs | Not documented in the admin guides |

## Events and payloads

You choose the events in the hook's **Select the Events to Send** card: expand a tool, then tick the create, update or delete events for the resources you want. **Selected Events** shows what the hook will send.

Procore's admin guides identify each delivered event by its **Resource Name**, **Resource ID**, **Event Type** and a UTC timestamp. The exact field names on the wire depend on the Payload Format you chose, and Procore documents them on its developer portal, in the "Introduction to Webhooks" and "Webhooks Payload Formats" guides. Capture a real delivery for your payload version before you write the parser.

Treat the event as a pointer to the record. If you need more than the identifiers your payload version carries, fetch the resource from the Procore REST API by its ID.

> **Inspect Procore webhooks as they arrive.** Point a hook at a [Hookdeck Inspect](https://inspect.hookdeck.com) URL to see the exact headers and body for your payload version, with no account or setup required.

## Setting up Procore webhooks

Generate a long random token first, for example with `openssl rand -hex 32`, and decide the full value you want Procore to send, such as `Bearer <token>`.

1. Open the company level **Admin** tool, and under **Company Settings** click **Webhooks**. For a single project, use the project level **Admin** tool and **Project Settings** > **Webhooks** instead.
2. Click **Create Hook**.
3. Enter your HTTPS endpoint in **Endpoint URL**.
4. Enter the full value in **Authorization Header**, for example `Bearer <token>`.
5. Choose the **Payload Format**. Agree it with whoever writes the receiver, because it can't be changed later.
6. In **Select the Events to Send**, tick the events you need, then check **Selected Events**.

To route through Hookdeck, create a source with the **Procore** type and use its URL as the endpoint URL. The source has one field, **Authorization Header**: paste the value exactly as you set it in Procore, including any `Bearer ` or `Basic ` prefix. The source compares the whole value and accepts `POST`.

## Securing Procore webhooks

Compare the whole `Authorization` header with the value you set on the hook, in constant time. Don't strip a prefix and don't base64-decode anything: Procore's own example, `Basic c3a24b8208ac5199d083d54a1234e94b8864`, is a token, not an encoded username and password.

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

// Exactly what you typed into Authorization Header, e.g. "Bearer 3f9c...".
const EXPECTED = process.env.PROCORE_AUTHORIZATION;

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/procore', express.raw({ type: '*/*' }), (req, res) => {
  const received = req.get('authorization');
  if (!EXPECTED || typeof received !== 'string' || !safeEqual(received, EXPECTED)) {
    return res.sendStatus(401);
  }

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

  // Queue the event, then respond. Fetch the full record from the REST API in a worker.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import hmac
import os

EXPECTED = os.environ.get("PROCORE_AUTHORIZATION", "")

def verify_procore_request(headers) -> bool:
    if not EXPECTED:
        return False  # fail closed when unconfigured
    received = headers.get("authorization")
    if not received:
        return False
    # Compare bytes: compare_digest raises TypeError on a non-ASCII str.
    return hmac.compare_digest(received.encode("utf-8"), EXPECTED.encode("utf-8"))

```

A static `Authorization` value 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, a long random token, and change the value on the hook and in your receiver if it might have leaked.

> **Make Procore webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) checks the Authorization Header value on every Procore event, stores each one durably, and lets you replay any delivery after a fix.

## Procore webhook limitations and pain points

### The Authorization Header is optional

**The Problem:** Requests arrive at your endpoint with nothing to authenticate them.

**Why It Happens:** Procore marks the field as optional, and a hook created without it sends no `Authorization` header.

**Workarounds:**

* Set the field on every hook, and fail closed when the header is missing.
* Check existing hooks, at company and project level, for an empty value.

**How Hookdeck Can Help:** Hookdeck's Procore source requires the value. Hookdeck answers Procore with a `200` either way, but a request with a missing or wrong header is recorded as failed verification and never reaches your destination.

### The payload version is locked in

**The Problem:** Moving to a newer payload format means rebuilding the hook.

**Why It Happens:** Procore fixes the Payload Format when the hook is created. To change it, you delete the hook and create a new one, and pick the events again.

**Workarounds:**

* Choose the version deliberately, and record it with the receiver's code.
* During a switch, run old and new hooks side by side and deduplicate on the resource ID and event type until the old one is gone.

**How Hookdeck Can Help:** Both hooks can point at the same Hookdeck source, and Hookdeck [filters](/docs/filters) and [transformations](/docs/transformations) can route or normalise the two formats while you migrate.

### Retries are visible but not documented

**The Problem:** You can see that Procore retried or discarded an event, but not when or how often it will try.

**Why It Happens:** The hook's Events view shows statuses such as Retrying, Failed and Discarded, but the admin guides don't give a retry schedule or timeout.

**Workarounds:**

* Respond with `200` quickly, and do the work afterwards.
* Check the hook's Events view after an outage, and re-sync anything marked Failed or Discarded from the REST API.

**How Hookdeck Can Help:** Hookdeck accepts each delivery as soon as it arrives, so Procore records it as delivered, then delivers it to your service with [retries](/docs/retries) you configure and lets you replay any event.

### Duplicates and ordering

**The Problem:** An update is applied twice, or an older update overwrites a newer one.

**Why It Happens:** Retried deliveries can repeat an event, and nothing in the admin guides promises ordering.

**Workarounds:**

* Deduplicate on the resource name, resource ID, event type and timestamp.
* Re-read the record from the REST API before applying a change, so you always write the current state.

**How Hookdeck Can Help:** Hookdeck's [deduplication](/docs/deduplication) can drop repeated deliveries based on the fields you choose before they reach your service.

## Best practices

### Store the full header value

Keep the whole value, prefix included, as one string and compare it whole. A comparison against the bare token rejects every request.

### Use one hook per purpose

Company and project hooks can both send to your endpoint. Keep track of which hook sends what, and use a separate endpoint path or value per hook if you need to tell them apart.

### Fetch, don't trust, the details

Use the identifiers in the event to read the record from the REST API before acting on it. That also protects you if an event arrives late.

### Acknowledge fast and process asynchronously

Verify the header, store the event, return `200`, and call the REST API from a worker. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## Conclusion

Procore hooks send a `POST` for each create, update or delete event on the resources you choose, authenticated only by the optional Authorization Header value. Set that value on every hook, compare the whole header in constant time, pick the payload version with care, and fetch records from the REST API when you need their full state.

[Hookdeck Event Gateway](https://hookdeck.com) sits in front of your receiver, checks the header, and keeps every event until your service has processed it.

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