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 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).

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, 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

bash
# 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.