Developers

ShutterReach API

Pipe inquiries into your CRM the moment couples reach out, log bookings from your studio software, and pull campaign results into your own reports.

REST + JSON

Predictable resource URLs, JSON bodies, standard HTTP status codes.

Scoped keys

Per-studio keys with only the permissions you grant. Revoke any time.

Signed webhooks

Real-time events with HMAC signatures and automatic retries.

Base URL

https://shutterreach.com/api/v1

Create a key in Settings → Developers (studio owners and admins only). Send it in the Authorization header on every request. Keys start with sr_live_ and are shown once, so store them in a secret manager, never in client-side code.

Each key belongs to one studio and carries the scopes you picked. Calls outside those scopes return 403 insufficient_scope.

curl https://shutterreach.com/api/v1/me \
  -H "Authorization: Bearer $SHUTTERREACH_API_KEY"
ScopeGrants
leads:readList and fetch inquiries.
leads:writeLog inquiries from other tools and move them through your pipeline.
campaigns:readCampaign settings, geofences and daily stats.
pages:readPage names, status and public URLs.
photos:readPhotos with CDN URLs for every rendition.

Errors use conventional HTTP status codes and a consistent body with a machine-readable code, a human-readable message, and the request id. Every response, successful or not, also carries the request id in the X-Request-Id header; send your own X-Request-Id to correlate with your logs.

{
  "error": {
    "code": "invalid_request",
    "message": "Provide an email or a phone number.",
    "details": [
      {
        "path": "email",
        "message": "Provide an email or a phone number."
      }
    ]
  },
  "request_id": "f84555f8-27f0-45b9-9abb-2b8866bbf3b0"
}
StatusCodeMeaning
400invalid_requestA parameter or body field failed validation. details lists each problem.
400invalid_jsonThe body isn't valid JSON.
400invalid_cursorThe pagination cursor is malformed.
401unauthorizedThe key is missing, malformed, expired or revoked.
403insufficient_scopeThe key doesn't have the scope this endpoint needs.
404not_foundNothing with that id exists in your studio.
415unsupported_media_typeSend bodies with Content-Type: application/json.
422invalid_referenceA referenced page or campaign belongs to another studio.
429rate_limitedSlow down. Retry after the number of seconds in Retry-After.
500internal_errorSomething broke on our side. Retrying is safe for GET requests.

List endpoints return newest items first in pages of up to 100 (limit, default 25). When has_more is true, pass next_cursor back as cursor to fetch the next page. Cursors are stable even while new items arrive.

let cursor;
do {
  const url = new URL("https://shutterreach.com/api/v1/leads");
  url.searchParams.set("limit", "100");
  if (cursor) url.searchParams.set("cursor", cursor);

  const response = await fetch(url, {
    headers: { Authorization: `Bearer ${process.env.SHUTTERREACH_API_KEY}` },
  });
  const page = await response.json();
  for (const lead of page.data) console.log(lead.name, lead.status);
  cursor = page.next_cursor;
} while (cursor);

Each key can make 600 requests per minute. Over the limit, requests return 429 rate_limited with a Retry-After header. For bulk syncs, page with limit=100 and prefer webhooks over polling.

Endpoints

Studio

The studio a key belongs to.

Get the current studio

get/v1/meany key

Response fields
object"studio"required
iduuidrequired
namestringrequired
slugstringrequired
citystring | nullrequired
regionstring | nullrequired
countrystringrequired
timezonestringrequired
currencystringrequired
planstringrequired
created_attimestamprequired
api_keyobjectrequired
curl "https://shutterreach.com/api/v1/me" \
  -H "Authorization: Bearer $SHUTTERREACH_API_KEY"
{
  "object": "studio",
  "id": "4d3c2b1a-7e6f-4a5b-9c8d-1e2f3a4b5c6d",
  "name": "Golden Hour Studio",
  "slug": "golden-hour-studio",
  "city": "Austin",
  "region": "TX",
  "country": "US",
  "timezone": "America/Chicago",
  "currency": "USD",
  "plan": "pro",
  "created_at": "2026-08-02T16:20:00.000Z",
  "api_key": {
    "id": "aab3238922bcc25a6f606eb525ffdc56",
    "scopes": [
      "leads:read",
      "leads:write",
      "campaigns:read"
    ]
  }
}

Endpoints

Inquiries

Couples who reached out (leads).

List inquiries

get/v1/leadsleads:read

Newest first. Spam is excluded unless you filter by status=spam.

Parameters

limitinteger
Page size, 1–100.

1–100 · default 25

cursorstring
next_cursor from the previous page.
statusenum
newcontactedconsult_bookedbookedlostspam
campaign_iduuid
page_iduuid
created_aftertimestamp
ISO 8601 timestamp (exclusive).
created_beforetimestamp
ISO 8601 timestamp (exclusive).

Returns a list envelope of Lead objects.

curl "https://shutterreach.com/api/v1/leads?limit=10" \
  -H "Authorization: Bearer $SHUTTERREACH_API_KEY"
{
  "object": "list",
  "data": [
    {
      "id": "8f14e45f-ceea-4673-a6d1-b2f5c3a9e0d1",
      "object": "lead",
      "status": "new",
      "name": "Maya Thompson",
      "partner_name": "Jordan Lee",
      "email": "maya@example.com",
      "phone": "+15125550142",
      "event_date": "2027-05-22",
      "event_date_flexible": false,
      "venue": "Laguna Gloria",
      "guest_count": 140,
      "budget": "$5–7k",
      "message": "We love your golden-hour portraits! Are you available for our May wedding?",
      "how_heard": "Instagram ad",
      "answers": {
        "coverage_hours": "8",
        "second_shooter": true
      },
      "source": "form",
      "campaign_id": "2b6f0cc9-04a3-4cbe-8d0b-5a1f6e7d9c3a",
      "page_id": "c9f0f895-fb98-4b9a-8f4e-1a2b3c4d5e6f",
      "utm": {
        "utm_source": "google",
        "utm_medium": "display",
        "utm_campaign": "spring-venues"
      },
      "booked_value_cents": null,
      "booked_at": null,
      "lost_reason": null,
      "created_at": "2026-10-14T18:22:05.118Z",
      "updated_at": "2026-10-14T18:22:05.118Z"
    }
  ],
  "has_more": true,
  "next_cursor": "WyIyMDI2LTEwLTE0VDE4OjIyOjA1LjExOFoiLCI4ZjE0ZTQ1ZiJd"
}

Create an inquiry

post/v1/leadsleads:write

Use this to log inquiries from your own forms, CRM or phone calls. Fires lead.created and notifies your team. An email or phone number is required.

Request body

namestringrequired
Primary contact's name.

max 160 chars

partner_namestring

max 160 chars

emailemail

max 254 chars

phonestring

max 40 chars

event_datedate
Wedding or session date, YYYY-MM-DD.
event_date_flexibleboolean
venuestring

max 200 chars

guest_countinteger

0–10000

budgetstring
Free text, e.g. "$4–6k".

max 80 chars

messagestring

max 5000 chars

how_heardstring

max 120 chars

answersobject
Answers to custom form questions, keyed by question id.
utmobject
Attribution parameters (utm_source, utm_campaign, …).
page_iduuid
Landing page the inquiry came from.
campaign_iduuid
Campaign to attribute the inquiry to.
Response fields
iduuidrequired
object"lead"required
statusenumrequired
newcontactedconsult_bookedbookedlostspam
namestringrequired
partner_namestring | nullrequired
emailstring | nullrequired
phonestring | nullrequired
event_datedate | nullrequired
event_date_flexiblebooleanrequired
venuestring | nullrequired
guest_countinteger | nullrequired

-9007199254740991–9007199254740991

budgetstring | nullrequired
messagestring | nullrequired
how_heardstring | nullrequired
answersobjectrequired
sourcestringrequired
form, api, manual, import
campaign_iduuid | nullrequired
page_iduuid | nullrequired
utmobjectrequired
booked_value_centsinteger | nullrequired

-9007199254740991–9007199254740991

booked_attimestamp | nullrequired
lost_reasonstring | nullrequired
created_attimestamprequired
updated_attimestamprequired
curl -X POST "https://shutterreach.com/api/v1/leads" \
  -H "Authorization: Bearer $SHUTTERREACH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Maya Thompson",
    "partner_name": "Jordan Lee",
    "email": "maya@example.com",
    "event_date": "2027-05-22",
    "venue": "Laguna Gloria",
    "message": "Called the studio line; wants an engagement session too.",
    "how_heard": "Phone"
  }'
{
  "id": "8f14e45f-ceea-4673-a6d1-b2f5c3a9e0d1",
  "object": "lead",
  "status": "new",
  "name": "Maya Thompson",
  "partner_name": "Jordan Lee",
  "email": "maya@example.com",
  "phone": "+15125550142",
  "event_date": "2027-05-22",
  "event_date_flexible": false,
  "venue": "Laguna Gloria",
  "guest_count": 140,
  "budget": "$5–7k",
  "message": "We love your golden-hour portraits! Are you available for our May wedding?",
  "how_heard": "Instagram ad",
  "answers": {
    "coverage_hours": "8",
    "second_shooter": true
  },
  "source": "form",
  "campaign_id": "2b6f0cc9-04a3-4cbe-8d0b-5a1f6e7d9c3a",
  "page_id": "c9f0f895-fb98-4b9a-8f4e-1a2b3c4d5e6f",
  "utm": {
    "utm_source": "google",
    "utm_medium": "display",
    "utm_campaign": "spring-venues"
  },
  "booked_value_cents": null,
  "booked_at": null,
  "lost_reason": null,
  "created_at": "2026-10-14T18:22:05.118Z",
  "updated_at": "2026-10-14T18:22:05.118Z"
}

Get an inquiry

get/v1/leads/{id}leads:read

Requires the leads:read scope.

Path parameters

iduuidrequired
Path parameter. The inquiry id.
Response fields
iduuidrequired
object"lead"required
statusenumrequired
newcontactedconsult_bookedbookedlostspam
namestringrequired
partner_namestring | nullrequired
emailstring | nullrequired
phonestring | nullrequired
event_datedate | nullrequired
event_date_flexiblebooleanrequired
venuestring | nullrequired
guest_countinteger | nullrequired

-9007199254740991–9007199254740991

budgetstring | nullrequired
messagestring | nullrequired
how_heardstring | nullrequired
answersobjectrequired
sourcestringrequired
form, api, manual, import
campaign_iduuid | nullrequired
page_iduuid | nullrequired
utmobjectrequired
booked_value_centsinteger | nullrequired

-9007199254740991–9007199254740991

booked_attimestamp | nullrequired
lost_reasonstring | nullrequired
created_attimestamprequired
updated_attimestamprequired
curl "https://shutterreach.com/api/v1/leads/8f14e45f-ceea-4673-a6d1-b2f5c3a9e0d1" \
  -H "Authorization: Bearer $SHUTTERREACH_API_KEY"
{
  "id": "8f14e45f-ceea-4673-a6d1-b2f5c3a9e0d1",
  "object": "lead",
  "status": "new",
  "name": "Maya Thompson",
  "partner_name": "Jordan Lee",
  "email": "maya@example.com",
  "phone": "+15125550142",
  "event_date": "2027-05-22",
  "event_date_flexible": false,
  "venue": "Laguna Gloria",
  "guest_count": 140,
  "budget": "$5–7k",
  "message": "We love your golden-hour portraits! Are you available for our May wedding?",
  "how_heard": "Instagram ad",
  "answers": {
    "coverage_hours": "8",
    "second_shooter": true
  },
  "source": "form",
  "campaign_id": "2b6f0cc9-04a3-4cbe-8d0b-5a1f6e7d9c3a",
  "page_id": "c9f0f895-fb98-4b9a-8f4e-1a2b3c4d5e6f",
  "utm": {
    "utm_source": "google",
    "utm_medium": "display",
    "utm_campaign": "spring-venues"
  },
  "booked_value_cents": null,
  "booked_at": null,
  "lost_reason": null,
  "created_at": "2026-10-14T18:22:05.118Z",
  "updated_at": "2026-10-14T18:22:05.118Z"
}

Update an inquiry

patch/v1/leads/{id}leads:write

Move an inquiry through your pipeline. Setting status: "booked" with booked_value_cents reports the booking and its value to Google Ads when the inquiry came from an ad click. Fires lead.updated when status or value changes.

Path parameters

iduuidrequired
Path parameter. The inquiry id.

Request body

statusenum
newcontactedconsult_bookedbookedlostspam
booked_value_centsinteger | null
Contract value; reported to Google Ads as conversion value when booked.

0–100000000

lost_reasonstring | null

max 200 chars

notesstring | null
Private internal notes.

max 5000 chars

Response fields
iduuidrequired
object"lead"required
statusenumrequired
newcontactedconsult_bookedbookedlostspam
namestringrequired
partner_namestring | nullrequired
emailstring | nullrequired
phonestring | nullrequired
event_datedate | nullrequired
event_date_flexiblebooleanrequired
venuestring | nullrequired
guest_countinteger | nullrequired

-9007199254740991–9007199254740991

budgetstring | nullrequired
messagestring | nullrequired
how_heardstring | nullrequired
answersobjectrequired
sourcestringrequired
form, api, manual, import
campaign_iduuid | nullrequired
page_iduuid | nullrequired
utmobjectrequired
booked_value_centsinteger | nullrequired

-9007199254740991–9007199254740991

booked_attimestamp | nullrequired
lost_reasonstring | nullrequired
created_attimestamprequired
updated_attimestamprequired
curl -X PATCH "https://shutterreach.com/api/v1/leads/8f14e45f-ceea-4673-a6d1-b2f5c3a9e0d1" \
  -H "Authorization: Bearer $SHUTTERREACH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "booked",
    "booked_value_cents": 650000
  }'
{
  "id": "8f14e45f-ceea-4673-a6d1-b2f5c3a9e0d1",
  "object": "lead",
  "status": "new",
  "name": "Maya Thompson",
  "partner_name": "Jordan Lee",
  "email": "maya@example.com",
  "phone": "+15125550142",
  "event_date": "2027-05-22",
  "event_date_flexible": false,
  "venue": "Laguna Gloria",
  "guest_count": 140,
  "budget": "$5–7k",
  "message": "We love your golden-hour portraits! Are you available for our May wedding?",
  "how_heard": "Instagram ad",
  "answers": {
    "coverage_hours": "8",
    "second_shooter": true
  },
  "source": "form",
  "campaign_id": "2b6f0cc9-04a3-4cbe-8d0b-5a1f6e7d9c3a",
  "page_id": "c9f0f895-fb98-4b9a-8f4e-1a2b3c4d5e6f",
  "utm": {
    "utm_source": "google",
    "utm_medium": "display",
    "utm_campaign": "spring-venues"
  },
  "booked_value_cents": null,
  "booked_at": null,
  "lost_reason": null,
  "created_at": "2026-10-14T18:22:05.118Z",
  "updated_at": "2026-10-14T18:22:05.118Z"
}

Endpoints

Campaigns

Geofenced ad campaigns and their daily results.

List campaigns

get/v1/campaignscampaigns:read

Requires the campaigns:read scope.

Parameters

limitinteger
Page size, 1–100.

1–100 · default 25

cursorstring
next_cursor from the previous page.
statusenum
draftscheduledlaunchingin_reviewactivepausedendedfailed

Returns a list envelope of Campaign objects.

curl "https://shutterreach.com/api/v1/campaigns?limit=10" \
  -H "Authorization: Bearer $SHUTTERREACH_API_KEY"
{
  "object": "list",
  "data": [
    {
      "id": "2b6f0cc9-04a3-4cbe-8d0b-5a1f6e7d9c3a",
      "object": "campaign",
      "name": "Spring venues · Austin",
      "playbook": "engagement_season",
      "status": "active",
      "review_status": "approved",
      "paused_reason": null,
      "daily_budget_cents": 2500,
      "start_date": "2026-10-01",
      "end_date": "2026-12-31",
      "landing_page_id": "c9f0f895-fb98-4b9a-8f4e-1a2b3c4d5e6f",
      "targeting": {
        "audiences": [
          "recently_engaged",
          "wedding_planning"
        ],
        "age_ranges": [
          "25_34",
          "35_44"
        ]
      },
      "bidding": {
        "strategy": "maximize_conversions"
      },
      "launched_at": "2026-10-01T15:04:11.000Z",
      "created_at": "2026-09-28T20:41:36.000Z",
      "updated_at": "2026-10-12T09:00:02.000Z"
    }
  ],
  "has_more": true,
  "next_cursor": "WyIyMDI2LTEwLTE0VDE4OjIyOjA1LjExOFoiLCI4ZjE0ZTQ1ZiJd"
}

Get a campaign

get/v1/campaigns/{id}campaigns:read

Includes the campaign's geofences.

Path parameters

iduuidrequired
Path parameter. The campaign id.
Response fields
iduuidrequired
object"campaign"required
namestringrequired
playbookenumrequired
engagement_seasonalways_onbridal_showvenue_spotlightcustom
statusenumrequired
draftscheduledlaunchingin_reviewactivepausedendedfailed
review_statusstring | nullrequired
paused_reasonstring | nullrequired
daily_budget_centsintegerrequired

-9007199254740991–9007199254740991

start_datedate | nullrequired
end_datedate | nullrequired
landing_page_iduuid | nullrequired
targetingobjectrequired
biddingobjectrequired
launched_attimestamp | nullrequired
created_attimestamprequired
updated_attimestamprequired
geofencesobject[]required
curl "https://shutterreach.com/api/v1/campaigns/2b6f0cc9-04a3-4cbe-8d0b-5a1f6e7d9c3a" \
  -H "Authorization: Bearer $SHUTTERREACH_API_KEY"
{
  "id": "2b6f0cc9-04a3-4cbe-8d0b-5a1f6e7d9c3a",
  "object": "campaign",
  "name": "Spring venues · Austin",
  "playbook": "engagement_season",
  "status": "active",
  "review_status": "approved",
  "paused_reason": null,
  "daily_budget_cents": 2500,
  "start_date": "2026-10-01",
  "end_date": "2026-12-31",
  "landing_page_id": "c9f0f895-fb98-4b9a-8f4e-1a2b3c4d5e6f",
  "targeting": {
    "audiences": [
      "recently_engaged",
      "wedding_planning"
    ],
    "age_ranges": [
      "25_34",
      "35_44"
    ]
  },
  "bidding": {
    "strategy": "maximize_conversions"
  },
  "launched_at": "2026-10-01T15:04:11.000Z",
  "created_at": "2026-09-28T20:41:36.000Z",
  "updated_at": "2026-10-12T09:00:02.000Z",
  "geofences": [
    {
      "id": "6512bd43-d9ca-4a6e-8f1b-2c3d4e5f6a7b",
      "kind": "venue",
      "label": "Laguna Gloria",
      "address": "3809 W 35th St, Austin, TX",
      "lat": 30.3129,
      "lng": -97.7713,
      "radius_m": 1609
    },
    {
      "id": "c20ad4d7-6fe9-4759-8a3b-4c5d6e7f8a9b",
      "kind": "bridal_shop",
      "label": "Brides of Austin",
      "address": "6501 N Lamar Blvd, Austin, TX",
      "lat": 30.3364,
      "lng": -97.7253,
      "radius_m": 805
    }
  ]
}

Get daily campaign results

get/v1/campaigns/{id}/statscampaigns:read

Impressions, clicks, spend and conversions per day, in the ads account's time zone. Today's numbers update every few hours.

Parameters

iduuidrequired
Path parameter. The campaign id.
fromdate
First day, YYYY-MM-DD (inclusive).
todate
Last day, YYYY-MM-DD (inclusive).
Response fields
object"campaign_stats"required
campaign_iduuidrequired
fromdate | nullrequired
todate | nullrequired
totalsobjectrequired
daysobject[]required
curl "https://shutterreach.com/api/v1/campaigns/2b6f0cc9-04a3-4cbe-8d0b-5a1f6e7d9c3a/stats" \
  -H "Authorization: Bearer $SHUTTERREACH_API_KEY"
{
  "object": "campaign_stats",
  "campaign_id": "2b6f0cc9-04a3-4cbe-8d0b-5a1f6e7d9c3a",
  "from": "2026-10-10",
  "to": "2026-10-12",
  "totals": {
    "impressions": 9432,
    "clicks": 87,
    "cost_cents": 7381,
    "conversions": 4,
    "conversion_value_cents": 600000
  },
  "days": [
    {
      "date": "2026-10-10",
      "impressions": 3110,
      "clicks": 31,
      "cost_cents": 2490,
      "conversions": 1,
      "conversion_value_cents": 0
    },
    {
      "date": "2026-10-11",
      "impressions": 3287,
      "clicks": 26,
      "cost_cents": 2452,
      "conversions": 2,
      "conversion_value_cents": 600000
    },
    {
      "date": "2026-10-12",
      "impressions": 3035,
      "clicks": 30,
      "cost_cents": 2439,
      "conversions": 1,
      "conversion_value_cents": 0
    }
  ]
}

Endpoints

Pages

Landing pages.

List landing pages

get/v1/pagespages:read

Requires the pages:read scope.

Parameters

statusenum
draftpublishedarchived

Returns a list envelope of Page objects.

curl "https://shutterreach.com/api/v1/pages" \
  -H "Authorization: Bearer $SHUTTERREACH_API_KEY"
{
  "object": "list",
  "data": [
    {
      "id": "c9f0f895-fb98-4b9a-8f4e-1a2b3c4d5e6f",
      "object": "page",
      "name": "Austin weddings",
      "slug": "austin-weddings",
      "is_home": false,
      "status": "published",
      "published_at": "2026-09-30T17:12:44.000Z",
      "url": "https://book.goldenhourstudio.com/austin-weddings",
      "created_at": "2026-09-20T14:02:11.000Z",
      "updated_at": "2026-09-30T17:12:44.000Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Endpoints

Photos

The photo library and ad renditions.

List photos

get/v1/photosphotos:read

Requires the photos:read scope.

Parameters

limitinteger
Page size, 1–100.

1–100 · default 25

cursorstring
next_cursor from the previous page.
ad_eligibleenum
truefalse
statusenum
uploadedprocessingreadyfailedrejected

Returns a list envelope of Photo objects.

curl "https://shutterreach.com/api/v1/photos?limit=10" \
  -H "Authorization: Bearer $SHUTTERREACH_API_KEY"
{
  "object": "list",
  "data": [
    {
      "id": "45c48cce-2e2d-4fbd-9c1e-7a8b9c0d1e2f",
      "object": "photo",
      "status": "ready",
      "ad_eligible": true,
      "consent_status": "granted",
      "session_type": "wedding",
      "title": "First look under the oaks",
      "alt_text": "Bride and groom share a first look beneath live oak trees at sunset",
      "ai_score": 92,
      "width": 6000,
      "height": 4000,
      "source": "upload",
      "urls": {
        "thumb": "https://cdn.example.com/media/thumb.webp",
        "w800": "https://cdn.example.com/media/w800.webp",
        "w1600": "https://cdn.example.com/media/w1600.webp",
        "ad_landscape": "https://cdn.example.com/media/ad_landscape.jpg",
        "ad_square": "https://cdn.example.com/media/ad_square.jpg"
      },
      "created_at": "2026-09-18T22:15:09.000Z"
    }
  ],
  "has_more": true,
  "next_cursor": "WyIyMDI2LTEwLTE0VDE4OjIyOjA1LjExOFoiLCI4ZjE0ZTQ1ZiJd"
}

Add an endpoint in Settings → Developers and choose the events it should receive. We send each event as a JSON POST with these headers:

X-ShutterReach-Event (the event type), X-ShutterReach-Delivery (unique per delivery), and X-ShutterReach-Signature (see below).

Respond with any 2xx within 10 seconds. Timeouts, network errors and 5xx/408/429 responses retry with exponential backoff, up to 10 attempts over about a day. Other 4xx responses stop retries. You can resend any failed delivery from the delivery log. Events can arrive out of order or more than once; use the top-level id to de-duplicate.

The signature header looks like t=1792199317,v1=5f8a…. Compute an HMAC-SHA256 of <t>.<raw body> using your endpoint's signing secret, compare it to v1 in constant time, and reject timestamps more than five minutes old to block replays. Rotating the secret in settings takes effect immediately.

Node.js

import crypto from "node:crypto";

// rawBody: the exact bytes we sent, before JSON parsing.
// secret: your endpoint's signing secret, including the "whsec_" prefix.
export function verifyShutterReachSignature(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const timestamp = Number(parts.t);
  if (!timestamp || Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest();
  const received = Buffer.from(parts.v1 ?? "", "hex");
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

Python

import hashlib, hmac, time

def verify_shutterreach_signature(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(item.split("=", 1) for item in header.split(","))
    timestamp = int(parts.get("t", "0"))
    if abs(time.time() - timestamp) > 300:
        return False
    signed = f"{timestamp}.".encode() + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))

Webhooks

Events

lead.created

A couple submitted an inquiry (or one was created via the API or manually).

{
  "id": "0f8fad5b-d9cb-469f-a165-70867728950e",
  "type": "lead.created",
  "created_at": "2026-10-14T18:22:05.402Z",
  "data": {
    "id": "8f14e45f-ceea-4673-a6d1-b2f5c3a9e0d1",
    "status": "new",
    "name": "Maya Thompson",
    "partner_name": "Jordan Lee",
    "email": "maya@example.com",
    "phone": "+15125550142",
    "event_date": "2027-05-22",
    "venue": "Laguna Gloria",
    "guest_count": 140,
    "budget": "$5–7k",
    "message": "We love your golden-hour portraits! Are you available for our May wedding?",
    "how_heard": "Instagram ad",
    "answers": {
      "coverage_hours": "8",
      "second_shooter": true
    },
    "source": "form",
    "campaign_id": "2b6f0cc9-04a3-4cbe-8d0b-5a1f6e7d9c3a",
    "page_id": "c9f0f895-fb98-4b9a-8f4e-1a2b3c4d5e6f",
    "utm": {
      "utm_source": "google",
      "utm_medium": "display",
      "utm_campaign": "spring-venues"
    },
    "booked_value_cents": null,
    "created_at": "2026-10-14T18:22:05.118Z",
    "updated_at": "2026-10-14T18:22:05.118Z",
    "studio_id": "4d3c2b1a-7e6f-4a5b-9c8d-1e2f3a4b5c6d"
  }
}

lead.updated

Status or booked value changed.

{
  "id": "0f8fad5b-d9cb-469f-a165-70867728950e",
  "type": "lead.updated",
  "created_at": "2026-10-14T18:22:05.402Z",
  "data": {
    "id": "8f14e45f-ceea-4673-a6d1-b2f5c3a9e0d1",
    "status": "new",
    "name": "Maya Thompson",
    "partner_name": "Jordan Lee",
    "email": "maya@example.com",
    "phone": "+15125550142",
    "event_date": "2027-05-22",
    "venue": "Laguna Gloria",
    "guest_count": 140,
    "budget": "$5–7k",
    "message": "We love your golden-hour portraits! Are you available for our May wedding?",
    "how_heard": "Instagram ad",
    "answers": {
      "coverage_hours": "8",
      "second_shooter": true
    },
    "source": "form",
    "campaign_id": "2b6f0cc9-04a3-4cbe-8d0b-5a1f6e7d9c3a",
    "page_id": "c9f0f895-fb98-4b9a-8f4e-1a2b3c4d5e6f",
    "utm": {
      "utm_source": "google",
      "utm_medium": "display",
      "utm_campaign": "spring-venues"
    },
    "booked_value_cents": null,
    "created_at": "2026-10-14T18:22:05.118Z",
    "updated_at": "2026-10-14T18:22:05.118Z",
    "studio_id": "4d3c2b1a-7e6f-4a5b-9c8d-1e2f3a4b5c6d"
  }
}

campaign.updated

Launched, approved, paused, resumed, ended, or budget changed.

{
  "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "type": "campaign.updated",
  "created_at": "2026-10-01T15:21:40.000Z",
  "data": {
    "change": "approved",
    "message": "Google approved your ads. They're serving now.",
    "campaign": {
      "id": "2b6f0cc9-04a3-4cbe-8d0b-5a1f6e7d9c3a",
      "name": "Spring venues · Austin",
      "status": "active",
      "review_status": "approved",
      "daily_budget_cents": 2500,
      "start_date": "2026-10-01",
      "end_date": "2026-12-31"
    }
  }
}

Changelog

  • 2026-10-16: API v1. Inquiries (read, create, update), campaigns with daily stats, pages, photos, and signed webhooks for inquiry and campaign events.