Guides

Webhooks

Get a POST to your endpoint every time someone scans a QR code on one of your cards.

Webhooks are not signed

Postaged doesn't sign webhook deliveries, so a request to your endpoint can't prove it came from us. Use an endpoint URL with a long, random path that only you and Postaged know, and treat every payload as untrusted input: check its values before you act on them.

Add an endpoint

  1. In Postaged, open Settings > API & Webhooks. Under Webhook endpoints, choose Add endpoint.
  2. Enter your Endpoint URL.
  3. Choose All campaigns, which includes campaigns you create later, or Selected campaigns.

Your endpoint must:

  • Use HTTPS on a public host. Redirects are not followed.
  • Answer within 10 seconds.

Only an Owner or Admin can add, pause, or delete an endpoint.

An endpoint URL that's hard to guess
https://hooks.example.com/postaged/7f3c1b9e5a2d48c6b0e4f19d7a6c3e85

The qr.scanned event

Every recorded QR scan from a matching campaign sends one POST with a JSON body. A value we don't have is null.

  • eventstring
    Always qr.scanned.
  • idstring
    The event id. The same value as the webhook-id header.
  • scanned_atstring
    When the scan happened, in UTC (ISO 8601).
  • campaignobject
    id (the campaign ID, like PA-4H2K9C) and name.
  • recipientobject
    Who the card was mailed to: first_name, last_name, email, phone, address_1, address_2, city, state, zip.
  • locationobject
    The approximate scan location: city, region, latitude, longitude.
  • destinationstring
    The address the QR code sent the person to.
  • testboolean
    Present and true only on a test event.

Headers

  • content-type: application/json
  • webhook-id: the event id. It stays the same across retries and redeliveries, so use it to deduplicate.
  • webhook-timestamp: when we sent this attempt, in Unix seconds.

There is no signature header. See Webhook events for the full schema.

Delivery
POST /postaged/7f3c1b9e5a2d48c6b0e4f19d7a6c3e85 HTTP/1.1
content-type: application/json
webhook-id: evt_8c1f2b9d4a
webhook-timestamp: 1786651282

{
  "event": "qr.scanned",
  "id": "evt_8c1f2b9d4a",
  "scanned_at": "2026-08-09T18:41:22Z",
  "campaign": { "id": "PA-4H2K9C", "name": "Spring open house" },
  "recipient": {
    "first_name": "Dana",
    "last_name": "Whitfield",
    "email": "dana@example.com",
    "phone": null,
    "address_1": "18 Rowan St",
    "address_2": null,
    "city": "Hartford",
    "state": "CT",
    "zip": "06105-2214"
  },
  "location": {
    "city": "Hartford",
    "region": "CT",
    "latitude": 41.76,
    "longitude": -72.69
  },
  "destination": "https://offers.example.com/SPRING24"
}

Respond quickly

Respond with any 2xx status as soon as you receive the event, then process it afterwards. Anything else, or no answer within 10 seconds, counts as a failed delivery.

Duplicates

  • Every scan delivers, including repeat scans from the same device. Deduplicate by webhook-id, which repeats only on a retry or when you choose Redeliver.
  • A retry, or a delivery sent again with Redeliver, carries the same webhook-id. Keep the ids you've processed and skip one you've already seen.
Your response
HTTP/1.1 200 OK

Retries and disabling

  • A failed delivery retries at about 5 minutes, 15 minutes, 1 hour, and 6 hours: 5 attempts in all.
  • After 20 failed delivery attempts in a row, we disable the endpoint and email your team.
  • While an endpoint is paused or disabled, scans aren't queued for it and aren't sent later.
  • To turn it back on, fix the endpoint, then choose Resume on it in Settings > API & Webhooks.
  • Recent deliveries on an endpoint shows its last 25 deliveries, and Redeliver sends one again.

Test events

Choose Send test event on an endpoint in Settings > API & Webhooks. We send a qr.scanned event with example data and a top-level "test": true.

  • A test delivery is never retried.
  • It never counts toward disabling the endpoint.
Test event
{
  "event": "qr.scanned",
  "id": "evt_test_5d0a9c3e7f21b846",
  "test": true,
  "scanned_at": "2026-08-09T18:41:22Z",
  "campaign": { "id": "PA-EXAMPLE", "name": "Example campaign" },
  "recipient": {
    "first_name": "Dana",
    "last_name": "Whitfield",
    "email": "dana@example.com",
    "phone": null,
    "address_1": "18 Rowan St",
    "address_2": null,
    "city": "Hartford",
    "state": "CT",
    "zip": "06105-2214"
  },
  "location": {
    "city": "Hartford",
    "region": "CT",
    "latitude": 41.76,
    "longitude": -72.69
  },
  "destination": "https://example.com/offers"
}