> ## Documentation Index
> Fetch the complete documentation index at: https://developers.meet-oj.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks and events

> Receive signed events, stream them live, or poll.

Every change to a referral creates an event. There are three ways to receive events:

| Method        | Use it for                                                                                   |
| ------------- | -------------------------------------------------------------------------------------------- |
| **Webhooks**  | Updating your system of record. Signed and retried for 72 hours.                             |
| **Streaming** | Live UIs and agents. `GET /referrals/{id}/events:stream` (SSE) resumes with `Last-Event-ID`. |
| **Polling**   | Clients that can't receive webhooks. `GET /events` returns the last 30 days, in order.       |

To get events for a single referral, register a per-referral webhook with its own token and expiry (`POST /referrals/{id}/webhooks`).

## Verify signatures

Deliveries follow [Standard Webhooks](https://www.standardwebhooks.com/). Each one carries `webhook-id`, `webhook-timestamp` and `webhook-signature` headers. Verify them with the secret returned when you registered the endpoint.

<CodeGroup>
  ```javascript Node theme={null}
  import { Webhook } from "standardwebhooks";

  const wh = new Webhook(process.env.OJ_WEBHOOK_SECRET);

  app.post("/webhooks/oj", express.raw({ type: "*/*" }), (req, res) => {
    const event = wh.verify(req.body, req.headers);
    if (event.type === "offer.received") notifyTeam(event.referral_id, event.data);
    res.sendStatus(204);
  });
  ```

  ```python Python theme={null}
  from standardwebhooks import Webhook

  wh = Webhook(os.environ["OJ_WEBHOOK_SECRET"])

  @app.post("/webhooks/oj")
  def oj_webhook():
      event = wh.verify(request.get_data(), request.headers)
      if event["type"] == "referral.funded":
          mark_funded(event["referral_id"], event["data"])
      return "", 204
  ```

  ```php PHP theme={null}
  use StandardWebhooks\Webhook;

  $wh = new Webhook(env('OJ_WEBHOOK_SECRET'));
  $event = $wh->verify($request->getContent(), $request->headers->all());
  if ($event['type'] === 'payout.updated') {
      PartnerLedger::sync($event['referral_id'], $event['data']);
  }
  return response()->noContent();
  ```
</CodeGroup>

Retries reuse the same `webhook-id`, so deduplicate on it.

## Event types

| Event                        | Fires when                                                          |
| ---------------------------- | ------------------------------------------------------------------- |
| `referral.received`          | O.J. accepted the referral                                          |
| `referral.matches_updated`   | The lenders the business matches changed                            |
| `referral.needs_partner`     | Your team needs to act (see `data.todos`)                           |
| `referral.needs_borrower`    | The borrower needs to act (see `data.todos`)                        |
| `referral.reminder_due`      | A next action is overdue and O.J. has no channel to send a reminder |
| `document_request.opened`    | A document was requested                                            |
| `document_request.satisfied` | A requested document was received                                   |
| `referral.submitted`         | The file was sent to lenders                                        |
| `offer.received`             | A lender made an offer                                              |
| `offer.accepted`             | The borrower accepted an offer                                      |
| `referral.funded`            | The loan funded                                                     |
| `referral.declined`          | A lender declined (`data.outcome` names it)                         |
| `referral.closed`            | The referral ended without a lender decision, for example no match  |
| `message.sent`               | O.J. sent a message (`data` is the Message)                         |
| `message.received`           | The borrower or your team sent a message                            |
| `submission.awaiting_review` | A submission is ready for your program's reviewer                   |
| `submission.decided`         | A decision was recorded on a submission                             |
| `payout.updated`             | A payout changed status                                             |
