PRODA API Docs

PRODA Webhooks

Overview

Webhooks allow your application to react to changes on PRODA in near real-time.

Subscribing to webhook events

To subscribe to webhook events you have to provide a URL and indicate which event types you are interested in.

See the PUT subscription endpoint for details.

PRODA will make HTTPS POST requests to the provided URL, with a body content described in Section Webhook event details.

Any PRODA user can subscribe to webhook events. Typically you would use an account set up for machine-to-machine authentication, so you can easily query the API for extra information relating to an event.

Event visibility & filters

Which events are potentially visible depends on user permissions and database membership.

A webhook subscription additionally includes an event type filter. You should only subscribe to events that you intend to handle to speed up delivery and to reduce load on your servers. See Section Webhook event details for a list of event types.

Pausing webhook delivery

You can pause and resume webhook delivery at any time using the pause endpoint. Doing so will ensure you do not miss events, unlike cancelling and recreating the whole subscription.

If you expect downtime, you can

  1. pause
  2. perform maintenance
  3. unpause

This ensures prompt resumption of event delivery compared to waiting for the next error retry, which will be exponentially delayed.

You can also pause during unexpected downtime. The error retry timer keeps running while event delivery is paused. Because PRODA will not attempt redelivery while paused, pausing stops the exponential increase. If the error retry inverval expires during the pause, PRODA will attempt redelivery immediately upon unpausing. If it the pause duration is shorter than the error retry interval, unpausing will not immediately retry errors.

Webhook event details

A webhook event is a JSON object with following structure.

interface EventCommon {
  audit_event_lsn: number; // Unique event identifier
  event_time: string; // ISO8601 date & time
}

interface DatabaseEvent extends EventCommon {
  object_database_id: number;
}

interface UserEvent extends EventCommon {
  object_user_id: number;
}

interface ExtractionCompleted extends DatabaseEvent {
  event_type: "extraction-completed";
  details: {
    extraction_id: number;
    rentroll_date: string; // ISO8601 date
    rentrolls: { property_id: number; rentroll_id: number }[];
  };
}

interface ExtractionStatusSetToIncomplete extends DatabaseEvent {
  event_type: "extraction-status-set-to-incomplete";
  details: {
    extraction_id: number;
    rentroll_date: string; // ISO8601 date
    rentrolls: { property_id: number; rentroll_id: number }[];
  };
}

interface ExtractionDeleted extends DatabaseEvent {
  event_type: "extraction-deleted";
  details: {
    extraction_id: number;
    rentroll_date: string; // ISO8601 date
    rentrolls: { property_id: number; rentroll_id: number }[];
  };
}

interface ProdaAssistantDatabaseAvailability extends DatabaseEvent {
  event_type: "proda-assistant-database-availability";
  details: {
    availability: "Available" | "Removed";
  };
}

interface UserAccessStatusChanged extends UserEvent {
  event_type: "user-access-status-changed";
  details: {
    access: "Granted" | "Revoked";
    accessType: "Audit" | "ProdaAssistant" | "Odin";
  };
}

interface PropertyCreated extends DatabaseEvent {
  event_type: "property-created";
  details: {
    property_id: number;
    property_name: string;
  };
}

interface PropertyUpdated extends DatabaseEvent {
  event_type: "property-updated";
  details: {
    property_id: number;
    property_name: string;
  };
}

interface PropertyDeleted extends DatabaseEvent {
  event_type: "property-deleted";
  details: {
    property_id: number;
    property_name: string;
  };
}

type Event =
  | ExtractionCompleted
  | ExtractionStatusSetToIncomplete
  | ExtractionDeleted
  | ProdaAssistantDatabaseAvailability
  | UserAccessStatusChanged
  | PropertyCreated
  | PropertyUpdated
  | PropertyDeleted;

Log sequence number (LSN)

Every webhook event has a unique, typically non-decreasing log sequence number, except the test event (which has LSN 0 and varying contents).

If you have multiple subscriptions with the same target URL and they have access to the same events, you may receive the same event multiple times, potentially even out of order. You can use the X-Webhook-Subscription-UserId header to identify the source of the event.

Otherwise, the most common reason for receiving duplicate events (same log sequence number) is a failure to respond promptly to events, see Section Responding to webhook events.

Only in exceedingly rare circumstances will PRODA send an event with a smaller log sequence number than that of any event sent before to the same subscription. (If you use the same URL in multiple subscriptions, this can happen regularly.)

For the most part you can assume that events happen approximately in order of their log sequence numbers. However, log sequence numbers do not strictly reflect any semantic order. Two events can occur simultaneously or even in reverse order of what their log sequence numbers indicate.

Note that there will usually be gaps in the observed log sequence numbers. This alone should not cause alarm. If you still suspect you have missed an event, double check your event type filters, see Section Subscribing to webhook events.

Event time

This is the approximate time the event was processed by PRODA. It does not precisely correspond to either the start nor the end of a user action.

Event time will not be strictly increasing. There may be duplicates and events may appear out of (event time) order.

Object meta data

The object_database_id field indicates which company database the event relates to, if any.

The object_user_id field indicates which user the event relates to, if any.

Responding to webhook events

You should acknowledge event reception quickly by responding with an HTTP status code of 200 and no body content.

PRODA will consider failure to respond promptly as an error and will attempt to redeliver the event. We recommend your webhook event handler perform no work before acknowledging receipt except writing the event to a queue. You can then drain the queue at your leisure. This avoids duplicated events and unneccessary delays while ensuring you do not miss events.

PRODA will not follow redirects. If you need to change your webhook subscription URL, see Section Subscribing to webhook events.

Security

Webhook listeners must support TLS (HTTPS). Self-signed certificates are not accepted.

Given that your webhook listener is accessible to the internet at large, you should not blindly trust any incoming event. PRODA signs webhook events. To verify authenticity you must follow these steps:

  1. Retrieve the PRODA public keys for webhook signing from the jwks.json endpoint. You should cache this for up to one hour.
  2. The X-Proda-Signature header contains a signed JSON Web Token (JWT). Verify the signature using a JWT library. The key ID kid must match one of PRODA's public keys. Reject the webhook event if the signature is invalid.
  3. You can check the iat (issued at) claim inside the the JWT against the current, nominal POSIX time. Reject the webhook event if the discrepancy is greater than, e.g., five minutes to guard against replay attacks.
  4. Hash the raw body of the webhook request using SHA256. Compare the result to the hex-encoded body_sha256 field in the payload of the signed JWT. Reject the webhook event if the body checksums do not match.

Errors

PRODA will retry a failed webhook event delivery with exponential backoff.

If you have scheduled downtime, you can pause webhook delivery, see Section Pausing webhook delivery. Doing so ensures prompt resumption of webhook event delivery once you unpause.