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
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 typeX-DayZero-Signature— hex-encoded HMAC-SHA256 of the raw request body, keyed with the endpoint secret
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 within 10 seconds to acknowledge. DayZero never follows redirects.
Retries and delivery history
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).
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, succeeded, retrying, failed), attempts, the last response_code and next_retry_at when a retry is queued. The list is cursor paginated.
Test and manage
# Send a synthetic event to every active endpoint subscribed to it
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; the response reports endpoints_notified and the delivery_id to look up. 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 |
| 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 |
| Square | /api/v1/webhooks/square |
payment.completed, payment.updated, payout.sent, refund.created — see Square |
| Ramp | /api/v1/webhooks/ramp |
transactions.* and bills.* events — see Ramp |
| Plaid | /api/v1/webhooks/plaid |
Bank connection updates, new-transaction notifications, reauthorization required — see 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.