# Webhooks

> Receive real-time notifications from Stripe, Shopify, and Plaid.

- **Category:** Guides
- HTML version: https://www.ondayzero.com/docs/guides/webhooks
- All guides: https://www.ondayzero.com/docs/guides/index.md
- Endpoint catalog: https://www.ondayzero.com/docs/reference/index.md

---

# Webhooks

Webhooks in DayZero go both ways:

- **Outbound webhooks** let *you* register an HTTPS URL and receive signed events when Order-to-Cash records change — orders countersigned, invoices sent, payments received. This is the part you integrate with.
- **Inbound webhooks** are how Stripe, Shopify, Plaid, Square and Ramp notify DayZero. They are set up automatically when you connect an integration; nothing to configure.

## Outbound Webhooks

### Register an endpoint

```bash
curl -X POST "https://api.ondayzero.com/api/v1/webhooks/o2c" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/hooks/dayzero",
    "event_types": ["order.countersigned", "invoice.sent", "payment.received"]
  }'
```

The response includes the plaintext `secret` **exactly once** — store it; later reads return the endpoint with the secret masked. Supply your own `secret` (32–255 characters) if you prefer. `url` must be HTTPS and resolve to a public address. `is_active: false` registers an endpoint without delivering to it yet.

### Event types

| Event | Emitted when | Payload fields (besides `event`) |
|-------|--------------|----------------------------------|
| `order.countersigned` | An Order-to-Cash order is countersigned | `order_id`, `contract_id`, `customer_id`, `order_value_cents` |
| `contract.activated` | A contract becomes active | `contract_id`, `status` |
| `invoice.drafted` | A contract invoice is drafted for a billing period | `contract_id`, `invoice_id`, `amount_cents`, `period_key` |
| `invoice.sent` | A contract invoice is sent to the customer | `contract_id`, `invoice_id`, `amount_cents` |
| `payment.received` | A payment is applied to a contract invoice | `invoice_id`, `contract_id`, `amount_cents`, `method`, `total_paid_cents`, `status` |
| `drawdown.approved` | Hours are approved against a contract | `contract_id`, `drawdown_id`, `hours` |
| `contract.completed` | A contract reaches its end | `contract_id`, `status` |

Every payload is a flat JSON object whose `event` field names the type. Amounts are in cents.

### Verify deliveries

Each delivery is a `POST` with `Content-Type: application/json` and two headers:

- `X-DayZero-Event` — the event type
- `X-DayZero-Signature` — hex-encoded HMAC-SHA256 of the **raw request body**, keyed with the endpoint secret

```python
import hmac, hashlib


def verify(secret: str, raw_body: bytes, signature: str) -> bool:
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)
```

Compute the HMAC over the bytes exactly as received — re-serialising the JSON will change the digest. Respond with any `2xx` at once to acknowledge, and do the work after: an attempt has 15 seconds in all, from looking up your host to the first 1 KB of your answer, and one that runs past it is stopped and counted as a failed attempt. Only that first 1 KB of the answer is read. DayZero never follows redirects.

### Retries and delivery history

A delivery is sent once the operation that produced the event has been saved, normally within a second, so the record it describes can always be fetched when your endpoint is called; an operation that fails and is rolled back sends nothing. Until its first attempt a delivery is `pending`, and every minute any delivery still `pending` ten seconds after it was written is sent. While an attempt is being made the delivery is `sending`; nothing else sends it meanwhile, and an attempt DayZero could not finish (it stopped mid-send) is counted as failed two minutes on and retried on the schedule below. A slow endpoint does not hold up other endpoints' deliveries.

A non-2xx response or a network failure marks the delivery `retrying` and re-sends it with exponential backoff — after 1 minute, 5 minutes, 30 minutes, 2 hours, then 8 hours. After six attempts in total it becomes `failed`. Retries carry the same body and a fresh signature, so make your handler idempotent (key on the record IDs in the payload).

```bash
curl "https://api.ondayzero.com/api/v1/webhooks/o2c/ENDPOINT_UUID/deliveries?limit=50" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID"
```

Each delivery reports `event_type`, `status` (`pending`, `sending`, `succeeded`, `retrying`, `failed`), `attempts`, the last `response_code` and `next_retry_at` while an attempt is queued. The list is [cursor paginated](/docs/guides/pagination).

`POST /api/v1/webhooks/o2c/{endpoint_id}/deliveries/{delivery_id}/redeliver` sends a delivery again during the call, as a new delivery linked to the original by `redelivery_of`, and returns it with the attempt's outcome: `status` (`succeeded`, or `retrying` with the receiver's `response_code` or the `error`), `response_code` and `attempts`. An original still waiting to be sent or retried is stopped and marked `failed`; one being sent at that moment is refused with `409` (`CREDIT_646`) until its attempt has finished.

### Test and manage

```bash
# Send a synthetic event to this one endpoint only (it must be active)
curl -X POST "https://api.ondayzero.com/api/v1/webhooks/o2c/ENDPOINT_UUID/test-fire?event_type=invoice.sent" \
  -H "Authorization: Bearer dz_your_token_here" \
  -H "x-business-id: YOUR_BUSINESS_ID"
```

Test payloads look like `{"event": "invoice.sent", "test": true, "emitted_at": "...", "business_id": "..."}` and are signed like real ones. No other endpoint on the business receives a test, so `endpoints_notified` is always 1. The attempt is made during the call, so the response also carries its `status`, `response_code` and the `delivery_id` to look up. A credit event type is sent in the credit envelope instead (see [Partner Credit Lines](/docs/guides/credit)). `GET /api/v1/webhooks/o2c` lists endpoints, `GET /api/v1/webhooks/o2c/{endpoint_id}` fetches one, and `DELETE /api/v1/webhooks/o2c/{endpoint_id}` removes it along with its delivery history.

Webhook emission is best-effort: a failing receiver never blocks the underlying business operation, so treat webhooks as a nudge and the REST API as the source of truth.

## Inbound Webhooks (integrations)

DayZero receives event notifications from connected services at `/api/v1/webhooks/{provider}`. These endpoints take no Bearer token — each request is authenticated by the provider's own signature scheme — and they are not something API consumers call.

| Source | Endpoint | What DayZero listens for |
|--------|----------|--------------------------|
| Stripe | `/api/v1/webhooks/stripe` | `invoice.finalized`, `invoice.paid`, `invoice.voided`, `payout.paid`, `payout.failed` / `payout.canceled` — see [Stripe](/docs/guides/stripe) |
| Shopify | `/api/v1/webhooks/shopify` | `payouts/create`, `payouts/update`, `disputes/create`, `disputes/update`, `app/uninstalled` and the GDPR topics. Orders and products are pulled on a schedule, not pushed — see [Shopify](/docs/guides/shopify) |
| Square | `/api/v1/webhooks/square` | `payment.completed`, `payment.updated`, `payout.sent`, `refund.created` — see [Square](/docs/guides/square) |
| Ramp | `/api/v1/webhooks/ramp` | `transactions.*` and `bills.*` events — see [Ramp](/docs/guides/ramp) |
| Plaid | `/api/v1/webhooks/plaid` | Bank connection updates, new-transaction notifications, reauthorization required — see [Plaid](/docs/guides/plaid) |

Processing follows the same pattern for each: verify the signature (Stripe's `Stripe-Signature`, Shopify's `X-Shopify-Hmac-Sha256`, Square's `x-square-hmacsha256-signature`, Ramp's per-subscription signing secret, Plaid's `Plaid-Verification` JWT), store the event, then update records or nudge the relevant summary posting. Per-sale events are deliberately *not* posted one journal entry at a time — the ledger only sees daily or per-payout summaries.
