Download OpenAPI specification:Download
Welcome to the Perfect Venue API docs. This API is available to Perfect Venue partners. If you're interested in building an integration, please reach out to eng@perfectvenue.com and we'll help you get set up.
This site was generated with Redoc and the OpenAPI/Swagger format.
Use this flow when building a new integration:
GET /venues to find the venues available to the authorizing Perfect Venue user.venue_id values in your system.POST /events, including contact details inline when you do not already have a contact_id.GET /events plus webhooks to keep your system in sync.All authenticated API requests use this base URL:
https://api.perfectvenue.com/api/v1
1. List venues
After a Perfect Venue user authorizes your integration, call GET /venues first. Venue IDs are required when creating events.
The venue response also includes venue-specific configuration such as event types, spaces, and custom fields.
curl https://api.perfectvenue.com/api/v1/venues \
-H "Authorization: Bearer ACCESS_TOKEN"
Save the id for each venue your integration should write to. We recommend mapping each
Perfect Venue venue_id to the equivalent resource in your system, such as a location,
merchant, store, or account.
2. Create an event
Create an event with a known contact_id, or pass a top-level contact object to create or reuse a contact
inline. Use start_time and end_time for new integrations; start_date and end_date are deprecated.
curl https://api.perfectvenue.com/api/v1/events \
-H "Authorization: Bearer ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"event": {
"venue_id": "VENUE_ID",
"name": "Henry's Wedding",
"start_time": "2026-08-15T18:00:00Z",
"end_time": "2026-08-15T23:00:00Z",
"group_size": 120,
"source": "Partner Integration",
"event_type": "Wedding",
"spaces": ["main-hall"],
"custom": {
"dietary_restrictions": "vegan"
}
},
"contact": {
"first_name": "Ada",
"last_name": "Lovelace",
"email": "ada@example.com",
"phone": "+14155550100"
},
"message": {
"text": "Interested in a Saturday evening reception."
}
}'
3. Sync events
Use GET /events to backfill or reconcile data. Always pass venue_id; use pagination for large result sets.
curl "https://api.perfectvenue.com/api/v1/events?venue_id=VENUE_ID&per_page=100&page=1" \
-H "Authorization: Bearer ACCESS_TOKEN"
For incremental syncs, filter by updated_at:
curl "https://api.perfectvenue.com/api/v1/events?venue_id=VENUE_ID&date_field=updated_at&date_from=2026-06-01T00:00:00Z&date_to=2026-06-30T23:59:59Z" \
-H "Authorization: Bearer ACCESS_TOKEN"
venue_id identifies a venue available to the authorizing Perfect Venue user and is required for creating contacts and events.contact_id identifies the event's primary contact.event_type must match an event type configured for the venue.spaces should contain space slugs from the venue's spaces array.custom values are keyed by the venue's custom field key.timezone when displaying local times to users.150000 for $1,500.00.Most validation, permission, and not-found errors use status: "fail" and include one or more
messages in data.
{
"status": "fail",
"data": [
{ "message": "End time must be after start time" }
]
}
Common error statuses:
400 Bad Request: validation failed or required parameters are missing.401 Unauthorized: the access token is missing, invalid, or expired.403 Forbidden: the token does not grant access to the requested venue or resource.404 Not Found: the requested resource does not exist or is not visible to the authorizing Perfect Venue user.429 Too Many Requests: the access token exceeded the rate limit. Rate-limit responses use
a problem-details style JSON body with title, status, and detail.The Perfect Venue API uses OAuth 2.0 Bearer tokens for authentication. Partner developers build and operate integrations, but a Perfect Venue user must log in and authorize the application before the integration can access venue data.
At a high level:
redirect_uri with an authorization code.Access is limited to the Perfect Venue user who authorized the application. For example, if that
user can access two venues, GET /venues returns those venues and API writes are limited to
resources that user can access.
Perfect Venue provides the following values during setup:
| Value | Description |
|---|---|
client_id |
Public identifier for your OAuth application |
client_secret |
Secret used by your server when exchanging authorization codes and refresh tokens |
redirect_uri |
Callback URL registered for your integration |
Keep the client_secret on your server. Do not expose it in browser code, mobile apps, logs,
or customer-visible configuration.
Direct a Perfect Venue user to the Authorization URL with the following query parameters. The user must log in and grant permission before your integration can receive an authorization code.
response_type: Set to "code"client_id: Your application's unique client ID, provided by Perfect Venueredirect_uri: The URL to which the user will be redirected after authorizationstate: (Optional but recommended) A random string to maintain state between the request and callback, helping to prevent CSRF attacksExample Authorization URL:
https://api.perfectvenue.com/oauth/authorize?response_type=code&client_id=CLIENT_ID&redirect_uri=REDIRECT_URI&state=RANDOM_STRING
Always validate the state value on the callback before exchanging the authorization code.
Once the Perfect Venue user has authorized your application, they will be redirected to the specified
redirect_uri with a code parameter in the query string. If authorization fails or the user
declines access, the redirect may include an OAuth error instead of code.
To exchange this authorization code for an access token, send a POST request to the Token URL with the following setup:
Authorization Header: Use Basic HTTP authentication, where the client_id and client_secret are
combined in the format client_id:client_secret and Base64 encoded. This encoded value is included in the
Authorization header as Basic <encoded_credentials>.grant_type: Set to "authorization_code"code: The authorization code received in the redirectredirect_uri: Must match the redirect_uri used the previous stepExample Token Request:
POST https://api.perfectvenue.com/oauth/token
Authorization: Basic BASE64_ENCODED(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=AUTHORIZATION_CODE&redirect_uri=REDIRECT_URI
Example Token Response:
{
"access_token": "ACCESS_TOKEN",
"token_type": "Bearer",
"expires_in": 7200,
"refresh_token": "REFRESH_TOKEN",
"created_at": 1782950400
}
Store tokens securely. Access tokens are short-lived. Refresh tokens should be treated like passwords because they can be exchanged for new access tokens.
Upon a successful token exchange, you’ll receive a JSON response with an access token.
Include this token in the Authorization header as Bearer <token> for all authenticated requests.
Access tokens represent the Perfect Venue user who authorized the application. API responses and writes are limited to the venues and resources that user can access in Perfect Venue.
Example API Request:
GET /api/v1/venues
Host: api.perfectvenue.com
Authorization: Bearer YOUR_ACCESS_TOKEN
Token responses will include a refresh token that can be exchanged for a new access token.
To exchange a refresh token for a new access token, send a POST request to the Token URL with the following setup:
Authorization Header: Use Basic HTTP authentication, where the client_id and client_secret are
combined in the format client_id:client_secret and Base64 encoded. This encoded value is included in the
Authorization header as Basic <encoded_credentials>.grant_type: Set to "refresh_token"refresh_token: The refresh token obtained from a previous token requestExample Refresh Token Request:
POST https://api.perfectvenue.com/oauth/token
Authorization: Basic BASE64_ENCODED(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=REFRESH_TOKEN
Your integration should refresh the token before it expires or when an API request returns
401 Unauthorized because the access token is expired. If a refresh request fails, send the
Perfect Venue user through the authorization flow again.
Use token revocation when a user disconnects your integration, when you rotate stored credentials, or when you no longer need access for a Perfect Venue user.
Send a POST request to the revocation endpoint with client authentication and the token you
want to revoke:
POST https://api.perfectvenue.com/oauth/revoke
Authorization: Basic BASE64_ENCODED(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded
token=ACCESS_OR_REFRESH_TOKEN
Request parameters:
| Parameter | Required | Description |
|---|---|---|
token |
Yes | Access token or refresh token to revoke |
token_type_hint |
No | Optional hint such as access_token or refresh_token |
Successful revocation returns 200 OK with an empty JSON body. The endpoint also returns
200 OK for missing, invalid, or already revoked tokens. If the token was issued to a
different OAuth client, Perfect Venue returns 403 Forbidden with unauthorized_client.
After revoking a token, remove it from your system. If your integration needs access again, send the Perfect Venue user through the OAuth authorization flow to receive new tokens.
| Status | Meaning | What to do |
|---|---|---|
401 Unauthorized |
The access token is missing, invalid, expired, or revoked | Refresh the token or send the user through OAuth again |
403 Forbidden |
The token is valid, but the authorizing user does not have access to the requested venue or resource | Ask the user to confirm their Perfect Venue permissions or authorize with a different user |
The API is rate limited to 100 requests per minute per access token. If you exceed this
limit, the API will respond with 429 Too Many Requests.
When a request is rate limited, the response includes headers that help you decide when to retry:
| Header | Description |
|---|---|
Retry-After |
Seconds to wait before retrying |
X-RateLimit-Limit |
Maximum requests allowed per minute |
X-RateLimit-Remaining |
Requests remaining in the current window |
X-RateLimit-Reset |
Unix timestamp when the current limit window resets |
When you receive a 429, wait before retrying. For API support, contact eng@perfectvenue.com.
Creates a new contact.
Perfect Venue does not enforce unique fields on contacts. If you want to avoid creating contacts with duplicate phone numbers or emails, we recommend storing a copy of the contact ID in your own database, and enforce unique fields there.
required | object (CreateContactInput) |
{- "contact": {
- "venue_id": "VENUE_ID",
- "first_name": "Ada",
- "last_name": "Lovelace",
- "email": "ada@example.com",
- "phone": "+14155550100"
}
}{- "status": "success",
- "data": {
- "id": "contact_QvUKwrV-Q7G2pw",
- "venue_id": "venue_kjKibRA_qyCf8g",
- "first_name": "Andrew",
- "last_name": "McPlanner",
- "email": "andrew.mcplanner@example.com",
- "phone": "5555555555"
}
}Get an individual contact by ID.
| contact_id | string The ID of the contact |
{- "status": "success",
- "data": {
- "id": "contact_QvUKwrV-Q7G2pw",
- "venue_id": "venue_kjKibRA_qyCf8g",
- "first_name": "Andrew",
- "last_name": "McPlanner",
- "email": "andrew.mcplanner@example.com",
- "phone": "5555555555"
}
}Update an individual contact by ID.
| contact_id | string The ID of the contact |
required | object (UpdateContactInput) |
{- "contact": {
- "email": "ada.updated@example.com",
- "phone": "+14155550101"
}
}{- "status": "success",
- "data": {
- "id": "contact_QvUKwrV-Q7G2pw",
- "venue_id": "venue_kjKibRA_qyCf8g",
- "first_name": "Andrew",
- "last_name": "McPlanner",
- "email": "andrew.mcplanner@example.com",
- "phone": "5555555555"
}
}Returns a paginated list of events for a venue. Requires a venue_id query parameter.
Results can be filtered by status, event type, source, spaces, contact, or date range, and sorted by a configurable field and direction.
Pagination uses page and per_page query parameters. page defaults to 1; values below 1 are treated as 1. per_page defaults to 25 and is clamped between 1 and 100.
Pagination metadata is returned in data.meta with current_page, per_page, total_count, and total_pages. The paginated event array is returned in data.events.
| venue_id required | string The ID of the venue to list events for |
| status | Array of strings Items Enum: "lead" "qualified" "proposal_sent" "confirmed" "balance_due" "completed" "lost" Filter by one or more event statuses |
| event_type | Array of strings Filter by one or more event type names configured for the venue |
| source | Array of strings Filter by one or more event sources |
| spaces | Array of strings Filter by one or more space slugs configured for the venue |
| contact_id | string Filter events by contact ID |
| order | string Default: "created_at" Enum: "created_at" "updated_at" "start_time" "name" Field to sort results by |
| sort_direction | string Default: "desc" Enum: "asc" "desc" Sort direction |
| date_from | string <date-time> Example: date_from=2025-01-01T00:00:00Z Start of date range filter (ISO 8601). Must be provided together with |
| date_to | string <date-time> Example: date_to=2025-12-31T23:59:59Z End of date range filter (ISO 8601). Must be provided together with |
| date_field | string Default: "created_at" Enum: "created_at" "updated_at" "start_time" Which date field the |
| page | integer >= 1 Default: 1 Page number |
| per_page | integer [ 1 .. 100 ] Default: 25 Number of results per page (max 100) |
{- "status": "success",
- "data": {
- "events": [
- {
- "id": "dfa4b137-1b3e-4927-a3d8-0e0e5bd601e3",
- "venue_id": "VENUE_ID",
- "contact_id": "CONTACT_ID",
- "name": "Henry's Wedding",
- "status": "lead",
- "start_date": "2026-08-15T18:00:00Z",
- "end_date": "2026-08-15T23:00:00Z",
- "start_time": "2026-08-15T18:00:00Z",
- "end_time": "2026-08-15T23:00:00Z",
- "group_size": 120,
- "source": "Partner Integration",
- "campaign": "Summer 2026",
- "amount": 150000,
- "event_type": "Wedding",
- "spaces": [
- "main-hall"
], - "created_at": "2026-06-16T17:00:00Z",
- "updated_at": "2026-06-16T17:00:00Z"
}
], - "meta": {
- "current_page": 1,
- "per_page": 25,
- "total_count": 1,
- "total_pages": 1
}
}
}Creates a new event in a venue.
Creating an event requires a venue_id. You can get a list of available venues for the authorizing Perfect Venue user by calling the list venues endpoint.
Creating an event requires venue_id, name, and either an event.contact_id or a top-level contact object. start_time and end_time are optional, but recommended for events with a known schedule.
When you create a contact via the create contact endpoint, you will get back a unique identifier for the contact that can be used to create events. You can also provide a top-level contact object to create or reuse a contact inline.
required | object (CreateEventInput) |
object Inline contact creation. Provide either | |
object An initial message from the contact to the venue, created along with the event |
{- "event": {
- "venue_id": "VENUE_ID",
- "contact_id": "CONTACT_ID",
- "name": "Henry's Wedding",
- "start_time": "2026-08-15T18:00:00Z",
- "end_time": "2026-08-15T23:00:00Z",
- "group_size": 120,
- "source": "Partner Integration",
- "campaign": "Summer 2026",
- "amount": 150000,
- "event_type": "Wedding",
- "spaces": [
- "main-hall"
], - "custom": {
- "dietary_restrictions": "vegan"
}
}, - "message": {
- "text": "Interested in a Saturday evening reception."
}
}{- "status": "success",
- "data": {
- "id": "dfa4b137-1b3e-4927-a3d8-0e0e5bd601e3",
- "venue_id": "venue_kjKibRA_qyCf8g",
- "contact_id": "contact_QvUKwrV-Q7G2pw",
- "name": "Henry's Wedding",
- "status": "confirmed",
- "start_date": "2024-11-12T10:00:00Z",
- "end_date": "2024-11-12T12:00:00Z",
- "start_time": "2024-11-12T10:00:00Z",
- "end_time": "2024-11-12T12:00:00Z",
- "group_size": 10,
- "source": "website",
- "campaign": "fall-2024",
- "amount": 150000,
- "event_type": "Wedding",
- "spaces": [
- "main-hall",
- "bridal-suite"
], - "created_at": "2026-01-15T09:00:00Z",
- "updated_at": "2026-06-01T14:30:00Z"
}
}Get an individual event by ID.
| event_id | string The ID of the event |
{- "status": "success",
- "data": {
- "id": "dfa4b137-1b3e-4927-a3d8-0e0e5bd601e3",
- "venue_id": "venue_kjKibRA_qyCf8g",
- "contact_id": "contact_QvUKwrV-Q7G2pw",
- "name": "Henry's Wedding",
- "status": "confirmed",
- "start_date": "2024-11-12T10:00:00Z",
- "end_date": "2024-11-12T12:00:00Z",
- "start_time": "2024-11-12T10:00:00Z",
- "end_time": "2024-11-12T12:00:00Z",
- "group_size": 10,
- "source": "website",
- "campaign": "fall-2024",
- "amount": 150000,
- "event_type": "Wedding",
- "spaces": [
- "main-hall",
- "bridal-suite"
], - "created_at": "2026-01-15T09:00:00Z",
- "updated_at": "2026-06-01T14:30:00Z"
}
}Update an individual event by ID.
| event_id | string The ID of the event |
required | object (UpdateEventInput) |
{- "event": {
- "start_time": "2026-08-15T19:00:00Z",
- "end_time": "2026-08-16T00:00:00Z",
- "group_size": 140,
- "amount": 175000
}
}{- "status": "success",
- "data": {
- "id": "dfa4b137-1b3e-4927-a3d8-0e0e5bd601e3",
- "venue_id": "venue_kjKibRA_qyCf8g",
- "contact_id": "contact_QvUKwrV-Q7G2pw",
- "name": "Henry's Wedding",
- "status": "confirmed",
- "start_date": "2024-11-12T10:00:00Z",
- "end_date": "2024-11-12T12:00:00Z",
- "start_time": "2024-11-12T10:00:00Z",
- "end_time": "2024-11-12T12:00:00Z",
- "group_size": 10,
- "source": "website",
- "campaign": "fall-2024",
- "amount": 150000,
- "event_type": "Wedding",
- "spaces": [
- "main-hall",
- "bridal-suite"
], - "created_at": "2026-01-15T09:00:00Z",
- "updated_at": "2026-06-01T14:30:00Z"
}
}List venues available to the Perfect Venue user who authorized your application. Perfect Venue provisions OAuth application access for partner developers during integration setup, but a Perfect Venue user must log in and grant permission before your integration can call the API.
{- "status": "success",
- "data": [
- {
- "id": "venue_kjKibRA_qyCf8g",
- "name": "The Lola Rose",
- "timezone": "America/New_York",
- "multi_day_enabled": true,
- "address": {
- "street_address1": "159 Osinski Crossroad",
- "street_address2": "Suite 255",
- "city": "South Zelda",
- "state": "North Dakota",
- "zip": "69196",
- "country": "United States"
}, - "event_types": [
- {
- "name": "Wedding"
}
], - "spaces": [
- {
- "slug": "main-hall",
- "name": "Main Hall",
- "standing": 200,
- "seated": 150
}
], - "custom_fields": [
- {
- "key": "dietary_restrictions",
- "name": "Dietary Restrictions",
- "data_type": "enum",
- "options": [
- "vegan",
- "gluten-free",
- "kosher"
]
}
]
}
]
}Get an individual venue by ID.
| venue_id | string The ID of the venue |
{- "status": "success",
- "data": {
- "id": "venue_kjKibRA_qyCf8g",
- "name": "The Lola Rose",
- "timezone": "America/New_York",
- "multi_day_enabled": true,
- "address": {
- "street_address1": "159 Osinski Crossroad",
- "street_address2": "Suite 255",
- "city": "South Zelda",
- "state": "North Dakota",
- "zip": "69196",
- "country": "United States"
}, - "event_types": [
- {
- "name": "Wedding"
}
], - "spaces": [
- {
- "slug": "main-hall",
- "name": "Main Hall",
- "standing": 200,
- "seated": 150
}
], - "custom_fields": [
- {
- "key": "dietary_restrictions",
- "name": "Dietary Restrictions",
- "data_type": "enum",
- "options": [
- "vegan",
- "gluten-free",
- "kosher"
]
}
]
}
}Perfect Venue can notify your application in real time when data changes for venues available to the authorizing Perfect Venue user. Use webhooks for incremental updates, and use the REST API to fetch the current resource state whenever you need to reconcile.
Quick start
Use this flow when adding webhook support:
POST requests from Perfect Venue.PV-Signature header on every delivery.PV-Webhook-Id as your idempotency key.2xx response only after you have safely accepted the delivery.Subscribing to events
Webhook subscriptions are configured by Perfect Venue during integration setup. To subscribe, send eng@perfectvenue.com:
| Field | Description |
|---|---|
| Endpoint URL | The HTTPS URL where Perfect Venue should send webhook deliveries |
| Event types | One or more public event type names from the table below |
| OAuth application | The OAuth application Perfect Venue configured for your integration |
Example setup request:
Endpoint URL: https://example.com/perfect-venue/webhooks
Event types: event.created, event.status_changed, event.deleted, contact.created
OAuth application: Partner Integration Name
Perfect Venue configures webhook subscriptions on your OAuth application. Your endpoint can subscribe to multiple event types.
Your endpoint must be reachable over HTTPS and should be prepared to receive duplicate deliveries.
Your endpoint receives events only for venues available to users who have authorized your OAuth application.
Delivery request
Each delivery is an HTTPS POST to your configured endpoint.
Required headers:
| Header | Description |
|---|---|
Content-Type |
Always application/json |
PV-Webhook-Id |
Stable delivery ID. Use this to deduplicate retries. |
PV-Signature |
Timestamped HMAC signature used to verify the request. |
Example delivery:
POST /perfect-venue/webhooks HTTP/1.1
Content-Type: application/json
PV-Webhook-Id: f8c1a0e2-6d2f-4a0d-99a2-1fd6f9f3c1aa
PV-Signature: t=1718557200,v1=5257a869e7ecebeda32affa62cdca3fa...
Payload envelope
The JSON body always follows this envelope:
{
"id": "f8c1a0e2-6d2f-4a0d-99a2-1fd6f9f3c1aa",
"type": "event.created",
"api_version": "2026-06-01",
"created_at": "2026-06-16T17:00:00Z",
"data": {
"id": "dfa4b137-1b3e-4927-a3d8-0e0e5bd601e3",
"venue_id": "VENUE_ID",
"contact_id": "CONTACT_ID",
"name": "Henry's Wedding",
"status": "lead",
"start_date": "2026-08-15T18:00:00Z",
"end_date": "2026-08-15T23:00:00Z",
"start_time": "2026-08-15T18:00:00Z",
"end_time": "2026-08-15T23:00:00Z",
"event_type": "Wedding",
"spaces": ["main-hall"],
"created_at": "2026-06-16T17:00:00Z",
"updated_at": "2026-06-16T17:00:00Z"
}
}
The data object contains the payload for the specific event type.
Event types
| Type | Fires when |
|---|---|
event.created |
An event (booking or lead) is created |
event.status_changed |
An event's status changes; payload includes the previous status |
event.deleted |
An event is deleted (soft deleted) |
contact.created |
A new inbound inquiry arrives |
Payloads
event.created and event.deleted
The data payload is the Event object — the same shape returned by
Get Event and List Events.
Example event.created payload:
{
"id": "f8c1a0e2-6d2f-4a0d-99a2-1fd6f9f3c1aa",
"type": "event.created",
"api_version": "2026-06-01",
"created_at": "2026-06-16T17:00:00Z",
"data": {
"id": "dfa4b137-1b3e-4927-a3d8-0e0e5bd601e3",
"venue_id": "VENUE_ID",
"contact_id": "CONTACT_ID",
"name": "Henry's Wedding",
"status": "lead",
"start_date": "2026-08-15T18:00:00Z",
"end_date": "2026-08-15T23:00:00Z",
"start_time": "2026-08-15T18:00:00Z",
"end_time": "2026-08-15T23:00:00Z",
"group_size": 120,
"source": "Partner Integration",
"campaign": "Summer 2026",
"amount": 150000,
"event_type": "Wedding",
"spaces": ["main-hall"],
"created_at": "2026-06-16T17:00:00Z",
"updated_at": "2026-06-16T17:00:00Z"
}
}
Example event.deleted payload:
{
"id": "9be6ab48-55f2-46f3-bd07-74af424c0f03",
"type": "event.deleted",
"api_version": "2026-06-01",
"created_at": "2026-06-17T15:30:00Z",
"data": {
"id": "dfa4b137-1b3e-4927-a3d8-0e0e5bd601e3",
"venue_id": "VENUE_ID",
"contact_id": "CONTACT_ID",
"name": "Henry's Wedding",
"status": "lost",
"start_date": "2026-08-15T18:00:00Z",
"end_date": "2026-08-15T23:00:00Z",
"start_time": "2026-08-15T18:00:00Z",
"end_time": "2026-08-15T23:00:00Z",
"event_type": "Wedding",
"spaces": ["main-hall"],
"created_at": "2026-06-16T17:00:00Z",
"updated_at": "2026-06-17T15:30:00Z"
}
}
contact.created
The data payload is the Contact object — the same shape returned by
Get Contact.
Only inbound inquiries emit this event.
Example contact.created payload:
{
"id": "7f6adfe6-9e01-4644-b850-3877b56526c8",
"type": "contact.created",
"api_version": "2026-06-01",
"created_at": "2026-06-16T17:00:00Z",
"data": {
"id": "CONTACT_ID",
"venue_id": "VENUE_ID",
"first_name": "Ada",
"last_name": "Lovelace",
"email": "ada@example.com",
"phone": "+14155550100"
}
}
event.status_changed
A thin payload carrying only what changed. Use the id to fetch the full event from the
REST API if you need other fields.
| Field | Type | Description |
|---|---|---|
id |
string | Event ID |
venue_id |
string | Venue ID |
status |
string | New status |
previous_status |
string|null | Status before the change |
updated_at |
string | ISO 8601 timestamp of the change |
Example:
{
"id": "f8c1a0e2-6d2f-4a0d-99a2-1fd6f9f3c1aa",
"type": "event.status_changed",
"api_version": "2026-06-01",
"created_at": "2026-06-16T17:00:00Z",
"data": {
"id": "dfa4b137-1b3e-4927-a3d8-0e0e5bd601e3",
"venue_id": "VENUE_ID",
"status": "confirmed",
"previous_status": "proposal_sent",
"updated_at": "2026-06-16T17:00:00Z"
}
}
Signature verification
Every delivery includes a PV-Signature header:
PV-Signature: t=1718557200,v1=5257a869e7ecebeda32affa62cdca3fa...
t is the Unix timestamp the request was signed. v1 is the HMAC-SHA256 hex digest of
"#{msg_id}.#{t}.#{raw_body}" where msg_id is the value of the PV-Webhook-Id header,
computed with your endpoint's signing secret.
Verification steps:
t and v1 from PV-Signature.t is too far from the current time. We recommend a 5-minute tolerance.PV-Webhook-Id + "." + t + "." + raw_body.v1 using a constant-time comparison.Including PV-Webhook-Id in the signed content means a replay attack with a swapped webhook ID
will fail verification.
Example Node.js verifier:
const crypto = require("crypto");
function verifyPerfectVenueWebhook({ rawBody, webhookId, signatureHeader, signingSecret }) {
const parts = Object.fromEntries(
signatureHeader.split(",").map((part) => part.split("="))
);
const timestamp = parts.t;
const expected = parts.v1;
const signedPayload = `${webhookId}.${timestamp}.${rawBody}`;
const actual = crypto
.createHmac("sha256", signingSecret)
.update(signedPayload)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(actual, "hex"),
Buffer.from(expected, "hex")
);
}
Idempotency
The PV-Webhook-Id header (and the id field in the body) is a stable identifier for an
event delivery. It does not change across retries. Use it to deduplicate: if you have already
processed an id, acknowledge with 2xx and ignore the repeat.
Delivery is at-least-once. Your handler must be safe to run twice for the same PV-Webhook-Id.
Response handling
Return a 2xx status after your endpoint has accepted the delivery. Any non-2xx response,
timeout, or connection error is treated as a failed delivery and retried.
Recommended handler behavior:
PV-Webhook-Id has already been processed.2xx.Ordering and sync
There is no global ordering guarantee. Deliveries are dispatched concurrently and can arrive
out of order. Events about the same resource carry a created_at you can order by, and you
can always read the current state from the REST API to reconcile. For high-confidence syncs,
combine webhooks with periodic GET /events backfills filtered by updated_at.
Retries and endpoint health
Failed deliveries are retried automatically.
If an endpoint fails repeatedly, it may be disabled until it is healthy again.
Sent when an event is created for a venue available to a Perfect Venue user who authorized your application.
| id required | string Stable webhook delivery ID. Matches the |
| type required | string Value: "event.created" |
| api_version required | string |
| created_at required | string <date-time> |
required | object Event object emitted by the webhook. This uses the same serializer as Get Event and List Events. |
{- "id": "f8c1a0e2-6d2f-4a0d-99a2-1fd6f9f3c1aa",
- "type": "event.created",
- "api_version": "2026-06-01",
- "created_at": "2026-06-16T17:00:00Z",
- "data": {
- "id": "dfa4b137-1b3e-4927-a3d8-0e0e5bd601e3",
- "venue_id": "VENUE_ID",
- "contact_id": "CONTACT_ID",
- "name": "Henry's Wedding",
- "status": "lead",
- "start_date": "2026-08-15T18:00:00Z",
- "end_date": "2026-08-15T23:00:00Z",
- "start_time": "2026-08-15T18:00:00Z",
- "end_time": "2026-08-15T23:00:00Z",
- "group_size": 120,
- "source": "Partner Integration",
- "campaign": "Summer 2026",
- "amount": 150000,
- "event_type": "Wedding",
- "spaces": [
- "main-hall"
], - "created_at": "2026-06-16T17:00:00Z",
- "updated_at": "2026-06-16T17:00:00Z"
}
}Sent when an event status changes. Fetch the event from the REST API if you need the full current event state.
| id required | string Stable webhook delivery ID. Matches the |
| type required | string Value: "event.status_changed" |
| api_version required | string |
| created_at required | string <date-time> |
required | object |
{- "id": "f8c1a0e2-6d2f-4a0d-99a2-1fd6f9f3c1aa",
- "type": "event.status_changed",
- "api_version": "2026-06-01",
- "created_at": "2026-06-16T17:00:00Z",
- "data": {
- "id": "dfa4b137-1b3e-4927-a3d8-0e0e5bd601e3",
- "venue_id": "VENUE_ID",
- "status": "confirmed",
- "previous_status": "proposal_sent",
- "updated_at": "2026-06-16T17:00:00Z"
}
}Sent when an event is deleted.
| id required | string Stable webhook delivery ID. Matches the |
| type required | string Value: "event.deleted" |
| api_version required | string |
| created_at required | string <date-time> |
required | object Deleted event object emitted by the webhook. Events are soft deleted, so the payload contains the event's last public shape. |
{- "id": "9be6ab48-55f2-46f3-bd07-74af424c0f03",
- "type": "event.deleted",
- "api_version": "2026-06-01",
- "created_at": "2026-06-17T15:30:00Z",
- "data": {
- "id": "dfa4b137-1b3e-4927-a3d8-0e0e5bd601e3",
- "venue_id": "VENUE_ID",
- "contact_id": "CONTACT_ID",
- "name": "Henry's Wedding",
- "status": "lost",
- "start_date": "2026-08-15T18:00:00Z",
- "end_date": "2026-08-15T23:00:00Z",
- "start_time": "2026-08-15T18:00:00Z",
- "end_time": "2026-08-15T23:00:00Z",
- "group_size": 120,
- "source": "Partner Integration",
- "campaign": "Summer 2026",
- "amount": 150000,
- "event_type": "Wedding",
- "spaces": [
- "main-hall"
], - "created_at": "2026-06-16T17:00:00Z",
- "updated_at": "2026-06-17T15:30:00Z"
}
}Sent when a new inbound inquiry creates a contact.
| id required | string Stable webhook delivery ID. Matches the |
| type required | string Value: "contact.created" |
| api_version required | string |
| created_at required | string <date-time> |
required | object Contact object emitted by the webhook. This uses the same serializer as Get Contact. |
{- "id": "7f6adfe6-9e01-4644-b850-3877b56526c8",
- "type": "contact.created",
- "api_version": "2026-06-01",
- "created_at": "2026-06-16T17:00:00Z",
- "data": {
- "id": "CONTACT_ID",
- "venue_id": "VENUE_ID",
- "first_name": "Ada",
- "last_name": "Lovelace",
- "email": "ada@example.com",
- "phone": "+14155550100"
}
}New Webhooks section
Outgoing webhooks are now available. Register an HTTPS endpoint on your OAuth application and subscribe to the event types you care about. Perfect Venue will POST a signed JSON payload to your endpoint whenever a subscribed event occurs.
Event types available in this release:
| Type | Fires when |
|---|---|
event.created |
An event is created |
event.status_changed |
An event's status changes |
event.deleted |
An event is deleted |
contact.created |
A new inbound inquiry arrives |
See the Webhooks section for the full delivery contract, payload schemas, signature verification, and retry behavior.
New List Events endpoint
GET /events is now available. Returns a paginated list of events for a venue with support for filtering
by status, event type, source, spaces, contact, and date range.
| Parameter | Type | Description |
|---|---|---|
venue_id |
string | Required. The ID of the venue |
status |
string[] | Filter by status: lead, qualified, proposal_sent, confirmed, balance_due, completed, lost |
event_type |
string[] | Filter by event type name |
source |
string[] | Filter by event source |
spaces |
string[] | Filter by space slug |
contact_id |
string | Filter by contact ID |
order |
string | Sort field: created_at (default), updated_at, start_time, name |
sort_direction |
string | asc or desc (default) |
date_from |
string | ISO 8601 start of date range (requires date_to) |
date_to |
string | ISO 8601 end of date range (requires date_from) |
date_field |
string | Date field for range filter: created_at (default), updated_at, start_time |
page |
integer | Page number (default: 1) |
per_page |
integer | Results per page, max 100 (default: 25) |
New fields on Get Venue and List Venues
| Field | Type | Description |
|---|---|---|
event_types |
object[] | Event types configured for the venue, each with a name |
spaces |
object[] | Spaces available at the venue, each with slug, name, standing, and seated capacity |
custom_fields |
object[] | Custom fields configured for the venue, each with key, name, data_type, and optional options |
New fields on Create Event and Update Event
The following optional fields are now supported:
| Field | Type | Description |
|---|---|---|
amount |
integer | Estimated event value in cents (e.g. 150000 for $1,500.00) |
event_type |
string | Event type name, must match a type configured for the venue |
spaces |
string[] | Space slugs to assign to the event |
custom |
object | Custom field values keyed by field key |
Inline contact creation on Create Event
You can now create a contact inline when creating an event by passing a top-level contact object
instead of a contact_id. If a contact with the given email already exists in your organization, it
will be reused.
Initial message on Create Event
You can now include an initial message from the contact to the venue when creating an event by passing
a top-level message object with a text field.
New Update Event endpoint
PATCH /events/{event_id} is now available. All fields are optional — only the fields you include
will be updated.