{
  "openapi": "3.1.0",
  "info": {
    "title": "Brandmerch Sends API",
    "version": "2026-10-09",
    "summary": "Create and track branded merchandise gifts.",
    "description": "Send branded merchandise from your Brandmerch stockroom programmatically, and follow every gift through webhooks.\n\n## Authentication\nSend your API key in the `X-API-Key` header. Keys start with `bm_live_`, belong to one company, and are created and revoked on Dashboard → Integrations. A key can be scoped to collections: it then sees only those designs, the gift links in them, and the sends it created. The API is available on Gifting & Distribution and Business.\n\n## Errors\nEvery refusal is JSON with an `error` message and a stable string `code`. Only authentication refusals (401, and the 403 for a deactivated account or a plan without API access), the inventory endpoint and the daily send cap (429) answer without a code. Match on `code`; messages can change. Some refusals add details next to it: `required`, `stockIssues`, `unresolved`, `reasons`, `status`, `parameter`. Error messages never contain recipient details.\n\nA body that is not valid JSON (for example a value with an unescaped double quote, pasted into a JSON template) is `400 invalid_json` on every route, before authentication. Fix the body: retrying it cannot succeed. A 500 with code `internal_error` is safe to retry.\n\nOne kind of request never reaches the API and answers in another shape: a body larger than 50 MB, or in an encoding or charset the server does not accept, is refused with HTTP 500 `{ \"status\": \"error\", \"code\": 1000, \"message\": \"Internal server error\" }` (a numeric `code`). Fix the request rather than retrying it.\n\n## Idempotency\nPass `idempotencyKey` (up to 200 characters, unique per company) on `POST /api/v1/sends`. Repeating the request with the same key and the same body returns the original send with its current status (HTTP 200, `idempotent: true`); the same key with a different body is `422 idempotency_key_reused`. The body is compared with keys sorted and `idempotencyKey` removed, so key order does not matter. Two identical requests racing each other create one send. A collection-scoped key that reuses another key's idempotencyKey also gets the 422.\n\n## Rate limits\n- `POST /api/v1/sends`: Gifting & Distribution is capped at 1,000 sends per rolling 24 hours per company (both modes); Business is unlimited. Successful creates carry `X-RateLimit-Limit` and `X-RateLimit-Remaining`; over the cap the answer is 429 with `Retry-After` and `X-RateLimit-Reset`. The cap is checked before the idempotency replay.\n- `POST /api/v1/sends/privacy-delete`: 60 requests per hour per company, then `429 rate_limited` with `Retry-After`.\n- Every route: at most 1,000 failed (4xx or 5xx) responses per 15 minutes per IP address; successful requests do not count. Over it, every request from that address answers `429 rate_limited` (JSON with `limit`, `remaining`, `reset` and `retryAfterSeconds`) with `Retry-After` until the window resets.\n\n## Webhooks\nSubscribe on Dashboard → Integrations → Webhooks with an HTTPS URL and the events you want; the signing secret (`whsec_example` in examples) is shown once. Each delivery is a POST of the envelope `{ event, sendId, data, timestamp }`, signed in `X-BrandMerch-Signature` (hex HMAC-SHA256 of the raw body). Answer 2xx within 10 seconds. Anything else is retried 10 times over about 24 hours (1, 2, 5, 15 and 30 minutes, then 1, 2, 4, 8 and 8 hours, with ±10% jitter); 410 stops retries. Redirects are not followed.\n\nDelivery order is not guaranteed — a retried event can arrive after a later one. Order a send's events by `timestamp` (the time the event was recorded; events recorded together are 1 ms apart) and dedupe on `X-BrandMerch-Event-Id`. When two events of one send share a timestamp, read `GET /api/v1/sends/{id}` for the current status.",
    "contact": {
      "name": "Brandmerch",
      "url": "https://brandmerch.com/developers"
    }
  },
  "servers": [
    {
      "url": "https://api.brandmerch.com"
    }
  ],
  "security": [
    {
      "ApiKeyAuth": []
    }
  ],
  "tags": [
    {
      "name": "Inventory",
      "description": "What can ship right now."
    },
    {
      "name": "Sends",
      "description": "Create, list, read and cancel sends."
    },
    {
      "name": "Recipient choice",
      "description": "Invitations for RECIPIENT_CHOICE sends."
    },
    {
      "name": "Privacy",
      "description": "Erase a recipient on request."
    },
    {
      "name": "Webhooks",
      "description": "Events Brandmerch posts to your subscription URL."
    }
  ],
  "paths": {
    "/api/v1/sends/inventory": {
      "get": {
        "operationId": "getInventory",
        "tags": [
          "Inventory"
        ],
        "summary": "Check inventory",
        "description": "Ready-to-ship inventory for a collection (a curated catalog) or for a list of design ids. Give exactly one of `collectionId` or `designIds`. Not metered. A collection-scoped key sees only its collections.",
        "parameters": [
          {
            "name": "collectionId",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "designIds",
            "in": "query",
            "description": "Comma-separated, or the parameter repeated. At most 200.",
            "style": "form",
            "explode": false,
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              },
              "maxItems": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Inventory. Designs that cannot be sent are listed under `unresolved` (designIds) or carry `produced: false` and a `notReadyReason` (collection).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/InventoryByCollection"
                    },
                    {
                      "$ref": "#/components/schemas/InventoryByDesigns"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Both or neither of collectionId and designIds, a malformed parameter, or more than 200 design ids. No `code`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`{ \"error\": \"Collection not found or not owned by this account\" }`. No `code`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/IpRateLimited"
          },
          "500": {
            "description": "`{ \"error\": \"Failed to fetch inventory\" }`. No `code`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sends": {
      "post": {
        "operationId": "createSend",
        "tags": [
          "Sends"
        ],
        "summary": "Create a send",
        "description": "**DIRECT**: you name the items and the address. Stock is reserved and the packages are queued in one step; the send is `reserved` and send.created then send.reserved are recorded.\n\n**RECIPIENT_CHOICE**: you name a recipient and an ON_SUBMIT gift link. The response carries the recipient's `redemptionUrl`; deliver it from your CRM or email tool (Brandmerch does not send invitations). No stock is reserved until the recipient chooses. send.created then send.invited are recorded.\n\nThe recipient picks what the gift link offers, from the sizes in stock at that moment: a set gift (every product the page shows, one option per product group, at the link's kit quantities) or a box they fill up to the link's per-recipient limit. Stock counts are never shown to them.\n\nMetered: on Gifting & Distribution, at most 1,000 sends per rolling 24 hours per company (both modes; the check runs before the idempotency replay). Unlimited on Business.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateSendRequest"
              },
              "examples": {
                "direct": {
                  "summary": "DIRECT",
                  "value": {
                    "idempotencyKey": "hubspot-deal-777-q4-thanks",
                    "recipient": {
                      "firstName": "Jane",
                      "lastName": "Smith",
                      "address1": "123 Main St",
                      "city": "San Francisco",
                      "state": "CA",
                      "zipCode": "94105",
                      "country": "US",
                      "email": "jane@example.com"
                    },
                    "items": [
                      {
                        "designId": "design_hoodie",
                        "quantity": 1,
                        "size": "M"
                      }
                    ],
                    "sender": {
                      "email": "rep@example.com"
                    },
                    "crm": {
                      "provider": "hubspot",
                      "objectType": "deal",
                      "objectId": "777"
                    },
                    "giftNote": "Welcome aboard, Jane!"
                  }
                },
                "recipientChoice": {
                  "summary": "RECIPIENT_CHOICE",
                  "value": {
                    "mode": "RECIPIENT_CHOICE",
                    "offer": {
                      "giftLinkId": "8d1f6a52-0c3e-4b7a-9f21-5e6d7c8b9a01"
                    },
                    "recipient": {
                      "email": "jane@example.com",
                      "name": "Jane Smith",
                      "company": "Acme"
                    },
                    "sender": {
                      "email": "rep@example.com"
                    },
                    "crm": {
                      "provider": "hubspot",
                      "portalId": "1234567",
                      "objectType": "contact",
                      "objectId": "901",
                      "ownerId": "55",
                      "dealId": "777"
                    },
                    "idempotencyKey": "hubspot-901-q4-thanks"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Idempotent replay: this idempotencyKey was already used with the same body. Returns the existing send with its CURRENT status and `idempotent: true`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateSendResponse"
                },
                "example": {
                  "sendId": "send_3f2a9c1e-7b4d-4e0a-9c6b-2d8f1a5e7c3b",
                  "orderId": "send_3f2a9c1e-7b4d-4e0a-9c6b-2d8f1a5e7c3b",
                  "status": "shipped",
                  "mode": "DIRECT",
                  "createdAt": "2026-10-09T12:00:00.000Z",
                  "idempotent": true
                }
              }
            }
          },
          "201": {
            "description": "Created.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateSendResponse"
                },
                "examples": {
                  "direct": {
                    "value": {
                      "sendId": "send_3f2a9c1e-7b4d-4e0a-9c6b-2d8f1a5e7c3b",
                      "orderId": "send_3f2a9c1e-7b4d-4e0a-9c6b-2d8f1a5e7c3b",
                      "status": "reserved",
                      "mode": "DIRECT",
                      "fulfillmentRequests": 1,
                      "createdAt": "2026-10-09T12:00:00.000Z"
                    }
                  },
                  "recipientChoice": {
                    "value": {
                      "sendId": "send_3f2a9c1e-7b4d-4e0a-9c6b-2d8f1a5e7c3b",
                      "orderId": null,
                      "status": "awaiting_redemption",
                      "mode": "RECIPIENT_CHOICE",
                      "redemptionUrl": "https://brandmerch.com/g/q4-thank-you?t=Xk3vQ9mP2rL8sT5wY1zA4bC7dE0fG6hJ2kM9nR3uV8x",
                      "expiresAt": "2026-11-08T12:00:00.000Z",
                      "createdAt": "2026-10-09T12:00:00.000Z"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The body was refused. Codes:\n- `invalid_json`: the body is not valid JSON (for example an unescaped double quote in a templated value). Fix the body; retrying it cannot succeed.\n- `invalid_request`: valid JSON that is not an object (an array), missing required fields (`required` lists them), `offer`/`expiresInDays` on a DIRECT body, or `notify` not a boolean\n- `invalid_mode`, `notify_unsupported` (`notify: true`)\n- `invalid_recipient`: incomplete address (`required` lists the fields) or a field of the wrong type\n- `invalid_recipient_email`, `invalid_country` (not ISO 3166-1 alpha-2 or a recognised name)\n- `invalid_items`: a line without designId, a quantity that is not an integer from 1 to 1,000, or a blank size\n- `too_many_items` (`maxLines`: 25 lines after combining)\n- `items_not_allowed` (RECIPIENT_CHOICE), `invalid_expires_in_days` (1 to 365)\n- `invalid_gift_note`, `invalid_gift_box`, `invalid_idempotency_key`, `invalid_metadata`, `invalid_crm`, `invalid_sender`\n- `designs_not_callable`: `unresolved` lists `{ designId, reason }` per design that cannot be sent",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "code": {
                          "enum": [
                            "invalid_json",
                            "invalid_request",
                            "invalid_mode",
                            "notify_unsupported",
                            "invalid_recipient",
                            "invalid_recipient_email",
                            "invalid_country",
                            "invalid_items",
                            "too_many_items",
                            "items_not_allowed",
                            "invalid_expires_in_days",
                            "invalid_gift_note",
                            "invalid_gift_box",
                            "invalid_idempotency_key",
                            "invalid_metadata",
                            "invalid_crm",
                            "invalid_sender",
                            "designs_not_callable"
                          ]
                        },
                        "required": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "field": {
                          "type": "string"
                        },
                        "maxLines": {
                          "type": "integer"
                        },
                        "unresolved": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "designId": {
                                "type": "string"
                              },
                              "reason": {
                                "type": "string"
                              }
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The API key is valid but cannot do this:\n- no `code`: the account is deactivated, or its plan has no API access (`upgrade_url`)\n- `recipient_choice_unavailable`: RECIPIENT_CHOICE is not enabled yet\n- `offer_out_of_key_scope`: the gift link is outside this key's collections\n- `sender_not_member`: the named sender is not an active member of your company\n- `sender_not_permitted`: the named sender may not send gifts (grant 'Can send gifts' on the Members page)",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "code": {
                          "enum": [
                            "recipient_choice_unavailable",
                            "offer_out_of_key_scope",
                            "sender_not_member",
                            "sender_not_permitted"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "`offer_not_found`: no such gift link in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "code": {
                          "enum": [
                            "offer_not_found"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "409": {
            "description": "- `insufficient_inventory`: `stockIssues` lists `{ designId, size, requested, available }` per short line (or `detail` when a bin ran out at reservation). Nothing was created.\n- `offer_not_ready`: `reasons` lists every precondition the gift link misses.\n- `no_default_sender`: no sender named and your company has no active owner or manager.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "code": {
                          "enum": [
                            "insufficient_inventory",
                            "offer_not_ready",
                            "no_default_sender"
                          ]
                        },
                        "stockIssues": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/StockIssue"
                          }
                        },
                        "detail": {
                          "type": "string"
                        },
                        "reasons": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/OfferNotReadyReason"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "422": {
            "description": "`idempotency_key_reused`: this idempotencyKey was used with a different body, or by another API key while this key is collection-scoped.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "code": {
                          "enum": [
                            "idempotency_key_reused"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "description": "- no `code`: the Gifting & Distribution daily cap (1,000 sends per rolling 24 hours); the body is `DailyLimitExceeded`\n- `rate_limited`: the per-IP failure limit (1,000 failed responses per 15 minutes per IP address)",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds until you can retry: until the oldest send in the window ages out (daily cap), or until the IP window resets."
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Unix seconds."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/DailyLimitExceeded"
                    },
                    {
                      "$ref": "#/components/schemas/IpRateLimitExceeded"
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure. Body: `{ \"error\": \"Failed to create send\", \"code\": \"internal_error\" }`. Retry with the same idempotencyKey: a create never runs twice.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "code": {
                          "enum": [
                            "internal_error"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "listSends",
        "tags": [
          "Sends"
        ],
        "summary": "List sends",
        "description": "Your company's sends, newest first, cursor-paged. A collection-scoped key lists only the sends it created. API orders created before sends existed are not listed (read them by id). Not metered.",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "description": "One or more statuses, comma-separated or repeated.",
            "style": "form",
            "explode": false,
            "schema": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/SendStatus"
              }
            }
          },
          {
            "name": "mode",
            "in": "query",
            "schema": {
              "$ref": "#/components/schemas/SendMode"
            }
          },
          {
            "name": "source",
            "in": "query",
            "schema": {
              "$ref": "#/components/schemas/SendSource"
            }
          },
          {
            "name": "giftLinkId",
            "in": "query",
            "schema": {
              "type": "string",
              "maxLength": 200
            }
          },
          {
            "name": "crmProvider",
            "in": "query",
            "description": "Matched lower-cased.",
            "schema": {
              "type": "string",
              "maxLength": 200
            }
          },
          {
            "name": "crmObjectId",
            "in": "query",
            "schema": {
              "type": "string",
              "maxLength": 200
            }
          },
          {
            "name": "senderUserId",
            "in": "query",
            "schema": {
              "type": "string",
              "maxLength": 200
            }
          },
          {
            "name": "createdAfter",
            "in": "query",
            "description": "ISO-8601, inclusive.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "createdBefore",
            "in": "query",
            "description": "ISO-8601, exclusive.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "`nextCursor` from the previous page.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of sends.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SendList"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_query`: `parameter` names the query parameter that was refused.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "code": {
                          "enum": [
                            "invalid_query"
                          ]
                        },
                        "parameter": {
                          "type": "string"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/IpRateLimited"
          },
          "500": {
            "description": "Unexpected failure. Body: `{ \"error\": \"Failed to list sends\", \"code\": \"internal_error\" }`. Safe to retry.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "code": {
                          "enum": [
                            "internal_error"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sends/{sendId}": {
      "get": {
        "operationId": "getSend",
        "tags": [
          "Sends"
        ],
        "summary": "Get a send",
        "description": "One send with its status, packages and tracking. A collection-scoped key reads only the sends it created. An API order created before sends existed is returned with `legacy: true` (unscoped keys only). Other orders are not readable here. Not metered.",
        "parameters": [
          {
            "name": "sendId",
            "in": "path",
            "required": true,
            "description": "The `sendId` returned when the send was created (`send_<uuid>`). An API order created before sends existed is read by its order id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The send.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SendView"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`send_not_found`",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "code": {
                          "enum": [
                            "send_not_found"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/IpRateLimited"
          },
          "500": {
            "description": "Unexpected failure. Body: `{ \"error\": \"Failed to fetch send status\", \"code\": \"internal_error\" }`. Safe to retry.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "code": {
                          "enum": [
                            "internal_error"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sends/{sendId}/cancel": {
      "post": {
        "operationId": "cancelSend",
        "tags": [
          "Sends"
        ],
        "summary": "Cancel a send",
        "description": "Cancels a send in `created`, `awaiting_redemption`, `redeemed`, `held` or `reserved`. A reserved send's stock is released; an awaiting invitation stops working. Refused once any package is being picked or has shipped. Records send.cancelled with your `reason`.",
        "parameters": [
          {
            "name": "sendId",
            "in": "path",
            "required": true,
            "description": "The `sendId` returned when the send was created (`send_<uuid>`). An API order created before sends existed is read by its order id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CancelSendRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Cancelled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CancelSendResponse"
                }
              }
            }
          },
          "400": {
            "description": "- `invalid_json`: the body is not valid JSON.\n- `invalid_request`: `reason` is not a string of at most 500 characters.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "code": {
                          "enum": [
                            "invalid_json",
                            "invalid_request"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "`send_not_found`",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "code": {
                          "enum": [
                            "send_not_found"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "409": {
            "description": "- `not_cancellable` (`status`): the send is already shipped, delivered or closed\n- `already_picked`: a package is being picked or packed, or has shipped\n- `cancel_incomplete`: a package could not be released; nothing was cancelled (contact support)\n- `status_changed`: the send changed while it was being cancelled; read it again\n- `legacy_send_not_cancellable`: an API order created before sends existed",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "code": {
                          "enum": [
                            "not_cancellable",
                            "already_picked",
                            "cancel_incomplete",
                            "status_changed",
                            "legacy_send_not_cancellable"
                          ]
                        },
                        "status": {
                          "$ref": "#/components/schemas/SendStatus"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/IpRateLimited"
          },
          "500": {
            "description": "Unexpected failure. Body: `{ \"error\": \"Failed to cancel send\", \"code\": \"internal_error\" }`. Safe to retry.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "code": {
                          "enum": [
                            "internal_error"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sends/{sendId}/resend": {
      "post": {
        "operationId": "resendInvitation",
        "tags": [
          "Recipient choice"
        ],
        "summary": "Resend an invitation",
        "description": "RECIPIENT_CHOICE: issues a new invitation for a send that is `awaiting_redemption` or `expired` (an expired send returns to `awaiting_redemption`). The previous redemption URL stops working. At most 3 resends per send. The gift link must still meet the creation preconditions. Records send.invited with the new URL. Not metered.",
        "parameters": [
          {
            "name": "sendId",
            "in": "path",
            "required": true,
            "description": "The `sendId` returned when the send was created (`send_<uuid>`). An API order created before sends existed is read by its order id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ResendRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Resent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResendResponse"
                },
                "example": {
                  "sendId": "send_3f2a9c1e-7b4d-4e0a-9c6b-2d8f1a5e7c3b",
                  "status": "awaiting_redemption",
                  "redemptionUrl": "https://brandmerch.com/g/q4-thank-you?t=Xk3vQ9mP2rL8sT5wY1zA4bC7dE0fG6hJ2kM9nR3uV8x",
                  "expiresAt": "2026-11-15T09:00:00.000Z",
                  "resendCount": 1
                }
              }
            }
          },
          "400": {
            "description": "- `invalid_json`: the body is not valid JSON.\n- `invalid_request`: valid JSON that is not an object (an array).\n- `invalid_expires_in_days`: not an integer from 1 to 365.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "code": {
                          "enum": [
                            "invalid_json",
                            "invalid_request",
                            "invalid_expires_in_days"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "No `code`: the account cannot use the API. `offer_out_of_key_scope`: the gift link is outside this key's collections.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "code": {
                          "enum": [
                            "offer_out_of_key_scope"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "`send_not_found`, or `offer_not_found` (the gift link was deleted).",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "code": {
                          "enum": [
                            "send_not_found",
                            "offer_not_found"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "409": {
            "description": "- `not_recipient_choice`: a DIRECT send has no invitation\n- `not_resendable` (`status`): the send is not awaiting redemption or expired, or the recipient's details were erased\n- `resend_limit_reached` (`resendCount`, `maxResends`)\n- `offer_not_ready` (`reasons`): the gift link no longer meets the preconditions\n- `status_changed`: a redemption, decline, cancel, expiry or another resend committed first; nothing was written",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "code": {
                          "enum": [
                            "not_recipient_choice",
                            "not_resendable",
                            "resend_limit_reached",
                            "offer_not_ready",
                            "status_changed"
                          ]
                        },
                        "status": {
                          "$ref": "#/components/schemas/SendStatus"
                        },
                        "resendCount": {
                          "type": "integer"
                        },
                        "maxResends": {
                          "type": "integer"
                        },
                        "reasons": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/OfferNotReadyReason"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/IpRateLimited"
          },
          "500": {
            "description": "Unexpected failure. Body: `{ \"error\": \"Failed to resend the invitation\", \"code\": \"internal_error\" }`. Safe to retry.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "code": {
                          "enum": [
                            "internal_error"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sends/privacy-delete": {
      "post": {
        "operationId": "requestPrivacyDelete",
        "tags": [
          "Privacy"
        ],
        "summary": "Erase a recipient's personal data",
        "description": "Queues the erasure of one recipient's personal data across your company's sends, orders, packages, gift link responses and invitations, and stored webhook payloads. Shipping labels and packing slips are deleted from storage. Sends that can still be cancelled are cancelled first.\n\nProcessed in the background within minutes: poll `GET /api/v1/sends/privacy-delete/{erasureId}` until its status is `completed`, `completed_with_exceptions` or `failed`. A request for an address that already has an open request (pending or running) returns that request (200, `existing: true`); after a request has finished, a new one is queued with a new erasureId.\n\nA package that is being picked or packed keeps its address so its label can print (exception `in_fulfillment`). Brandmerch then queues a follow-up request for the same address, with a new erasureId, that runs about 24 hours later and again every 24 hours until nothing is left in fulfillment or 30 days have passed since the original request. Filing the same address again returns that follow-up (200, `existing: true`) and runs it at once.\n\nA collection-scoped API key cannot file or read privacy requests (`403 scoped_key_not_allowed`): an erasure covers your whole company, including sends made with other keys. Use a key without collection scope.\n\nAccepted even if your plan has lapsed. Limited to 60 requests per hour per company. Copies already delivered to your own systems (for example stored webhook payloads) are yours to erase.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PrivacyDeleteRequest"
              },
              "example": {
                "email": "jane@example.com"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "This address already has an open request; it is returned.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PrivacyDeleteQueued"
                }
              }
            }
          },
          "202": {
            "description": "Queued.",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds until the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PrivacyDeleteQueued"
                },
                "example": {
                  "erasureId": "2f6c1b8e-4a3d-4c9e-8b7a-6d5e4f3c2b1a",
                  "status": "pending",
                  "requestedAt": "2026-10-09T12:00:00.000Z"
                }
              }
            }
          },
          "400": {
            "description": "- `invalid_json`: the body is not valid JSON.\n- `invalid_email`: the email is missing or not an address.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "code": {
                          "enum": [
                            "invalid_json",
                            "invalid_email"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- no `code`: `{ \"error\": \"Account is deactivated\" }`\n- `scoped_key_not_allowed`: the API key is scoped to collections. Use a key without collection scope.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "code": {
                          "enum": [
                            "scoped_key_not_allowed"
                          ]
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "error": "A collection-scoped API key cannot make privacy requests",
                  "code": "scoped_key_not_allowed"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited`, for either limit:\n- more than 60 requests in an hour for your company: `{ error, code }`\n- the per-IP failure limit (1,000 failed responses per 15 minutes per IP address): `IpRateLimitExceeded`",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds until the window resets."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/Error"
                        },
                        {
                          "properties": {
                            "error": {
                              "const": "Too many privacy-delete requests"
                            },
                            "code": {
                              "enum": [
                                "rate_limited"
                              ]
                            }
                          }
                        }
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/IpRateLimitExceeded"
                    }
                  ]
                },
                "example": {
                  "error": "Too many privacy-delete requests",
                  "code": "rate_limited"
                }
              }
            }
          },
          "500": {
            "description": "`internal_error`",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "code": {
                          "enum": [
                            "internal_error"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "503": {
            "description": "`privacy_unavailable`: requests cannot be accepted right now. Try again later.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "code": {
                          "enum": [
                            "privacy_unavailable"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sends/privacy-delete/{erasureId}": {
      "get": {
        "operationId": "getPrivacyDelete",
        "tags": [
          "Privacy"
        ],
        "summary": "Get a privacy request",
        "description": "The request and its counts. Never returns the email address. Accepted even if your plan has lapsed. `completed`, `completed_with_exceptions` and `failed` are final: stop polling on any of them, and contact support about a `failed` request. A collection-scoped API key cannot file or read privacy requests (`403 scoped_key_not_allowed`): an erasure covers your whole company, including sends made with other keys. Use a key without collection scope.",
        "parameters": [
          {
            "name": "erasureId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PiiErasureView"
                },
                "example": {
                  "erasureId": "2f6c1b8e-4a3d-4c9e-8b7a-6d5e4f3c2b1a",
                  "status": "completed",
                  "requestedAt": "2026-10-09T12:00:00.000Z",
                  "completedAt": "2026-10-09T12:03:10.000Z",
                  "tablesTouched": {
                    "sends": 2,
                    "send_events": 9,
                    "orders": 1,
                    "warehouse_fulfillment_requests": 1
                  },
                  "exceptionsCount": 0,
                  "exceptionsByReason": {},
                  "s3KeysDeletedCount": 2
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "- no `code`: `{ \"error\": \"Account is deactivated\" }`\n- `scoped_key_not_allowed`: the API key is scoped to collections. Use a key without collection scope.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "code": {
                          "enum": [
                            "scoped_key_not_allowed"
                          ]
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "error": "A collection-scoped API key cannot make privacy requests",
                  "code": "scoped_key_not_allowed"
                }
              }
            }
          },
          "404": {
            "description": "`erasure_not_found`",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "code": {
                          "enum": [
                            "erasure_not_found"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/IpRateLimited"
          },
          "500": {
            "description": "`internal_error`",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "code": {
                          "enum": [
                            "internal_error"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    }
  },
  "webhooks": {
    "send.created": {
      "post": {
        "summary": "A send was created",
        "description": "Every send, once. DIRECT: `data.status` is `reserved` (the order and its reservation commit together) and `fulfillmentRequests` counts the packages. RECIPIENT_CHOICE: `data.status` is `awaiting_redemption` and `fulfillmentRequests` is 0.",
        "operationId": "webhook_send_created",
        "tags": [
          "Webhooks"
        ],
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/X-BrandMerch-Signature"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Event"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Event-Id"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Delivery-Id"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Attempt"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendCreatedEvent"
              }
            }
          }
        },
        "responses": {
          "410": {
            "description": "Gone: Brandmerch stops retrying this delivery."
          },
          "2XX": {
            "description": "Delivered. Answer within 10 seconds; do the work afterwards."
          },
          "default": {
            "description": "Any other status, a redirect, a timeout or a network error is retried: 11 attempts over about 24 hours."
          }
        }
      }
    },
    "send.invited": {
      "post": {
        "summary": "An invitation was issued",
        "description": "RECIPIENT_CHOICE: on create (1 ms after send.created) and on every resend. Carries the redemption URL to deliver to the recipient; a resend makes the previous URL stop working.",
        "operationId": "webhook_send_invited",
        "tags": [
          "Webhooks"
        ],
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/X-BrandMerch-Signature"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Event"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Event-Id"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Delivery-Id"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Attempt"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendInvitedEvent"
              },
              "example": {
                "event": "send.invited",
                "sendId": "send_3f2a9c1e-7b4d-4e0a-9c6b-2d8f1a5e7c3b",
                "data": {
                  "eventId": "5b2e7c1a-9d4f-4e8b-a3c6-0f1e2d3c4b5a",
                  "sendId": "send_3f2a9c1e-7b4d-4e0a-9c6b-2d8f1a5e7c3b",
                  "orderId": null,
                  "companyId": "c0a8012e-5f4b-4d3c-8a2b-1e9f8d7c6b5a",
                  "mode": "RECIPIENT_CHOICE",
                  "source": "API",
                  "status": "awaiting_redemption",
                  "holdReason": null,
                  "recipient": {
                    "email": "jane@example.com",
                    "name": "Jane Smith",
                    "company": "Acme",
                    "city": null,
                    "state": null
                  },
                  "sender": {
                    "userId": "usr_example",
                    "email": "rep@example.com",
                    "name": "Alex Rep",
                    "externalId": null,
                    "unmapped": false
                  },
                  "offer": {
                    "giftLinkId": "8d1f6a52-0c3e-4b7a-9f21-5e6d7c8b9a01",
                    "title": "Q4 thank-you"
                  },
                  "items": [],
                  "crm": {
                    "provider": "hubspot",
                    "portalId": "1234567",
                    "objectType": "contact",
                    "objectId": "901",
                    "ownerId": "55",
                    "dealId": "777",
                    "workflowId": null,
                    "callbackId": null
                  },
                  "metadata": null,
                  "tracking": null,
                  "redemptionUrl": "https://brandmerch.com/g/q4-thank-you?t=Xk3vQ9mP2rL8sT5wY1zA4bC7dE0fG6hJ2kM9nR3uV8x",
                  "expiresAt": "2026-11-08T12:00:00.000Z",
                  "resendCount": 0
                },
                "timestamp": "2026-10-09T12:00:00.001Z"
              }
            }
          }
        },
        "responses": {
          "410": {
            "description": "Gone: Brandmerch stops retrying this delivery."
          },
          "2XX": {
            "description": "Delivered. Answer within 10 seconds; do the work afterwards."
          },
          "default": {
            "description": "Any other status, a redirect, a timeout or a network error is retried: 11 attempts over about 24 hours."
          }
        }
      }
    },
    "send.viewed": {
      "post": {
        "summary": "The recipient opened the invitation",
        "description": "The first time the recipient opens an invitation. A resend issues a new invitation, which records its own send.viewed.",
        "operationId": "webhook_send_viewed",
        "tags": [
          "Webhooks"
        ],
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/X-BrandMerch-Signature"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Event"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Event-Id"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Delivery-Id"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Attempt"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendViewedEvent"
              }
            }
          }
        },
        "responses": {
          "410": {
            "description": "Gone: Brandmerch stops retrying this delivery."
          },
          "2XX": {
            "description": "Delivered. Answer within 10 seconds; do the work afterwards."
          },
          "default": {
            "description": "Any other status, a redirect, a timeout or a network error is retried: 11 attempts over about 24 hours."
          }
        }
      }
    },
    "send.redeemed": {
      "post": {
        "summary": "The recipient chose",
        "description": "RECIPIENT_CHOICE. `data.items` is the recipient's choice; `data.status` is `redeemed`. Followed by send.reserved, or by send.held and send.exception.",
        "operationId": "webhook_send_redeemed",
        "tags": [
          "Webhooks"
        ],
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/X-BrandMerch-Signature"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Event"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Event-Id"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Delivery-Id"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Attempt"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendRedeemedEvent"
              }
            }
          }
        },
        "responses": {
          "410": {
            "description": "Gone: Brandmerch stops retrying this delivery."
          },
          "2XX": {
            "description": "Delivered. Answer within 10 seconds; do the work afterwards."
          },
          "default": {
            "description": "Any other status, a redirect, a timeout or a network error is retried: 11 attempts over about 24 hours."
          }
        }
      }
    },
    "send.declined": {
      "post": {
        "summary": "The recipient declined the gift",
        "description": "RECIPIENT_CHOICE. Final.",
        "operationId": "webhook_send_declined",
        "tags": [
          "Webhooks"
        ],
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/X-BrandMerch-Signature"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Event"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Event-Id"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Delivery-Id"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Attempt"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendDeclinedEvent"
              }
            }
          }
        },
        "responses": {
          "410": {
            "description": "Gone: Brandmerch stops retrying this delivery."
          },
          "2XX": {
            "description": "Delivered. Answer within 10 seconds; do the work afterwards."
          },
          "default": {
            "description": "Any other status, a redirect, a timeout or a network error is retried: 11 attempts over about 24 hours."
          }
        }
      }
    },
    "send.expired": {
      "post": {
        "summary": "The invitation expired unredeemed",
        "description": "RECIPIENT_CHOICE. Recorded by an hourly job after `expiresAt`. A resend reopens the send.",
        "operationId": "webhook_send_expired",
        "tags": [
          "Webhooks"
        ],
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/X-BrandMerch-Signature"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Event"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Event-Id"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Delivery-Id"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Attempt"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendExpiredEvent"
              }
            }
          }
        },
        "responses": {
          "410": {
            "description": "Gone: Brandmerch stops retrying this delivery."
          },
          "2XX": {
            "description": "Delivered. Answer within 10 seconds; do the work afterwards."
          },
          "default": {
            "description": "Any other status, a redirect, a timeout or a network error is retried: 11 attempts over about 24 hours."
          }
        }
      }
    },
    "send.held": {
      "post": {
        "summary": "The choice could not be reserved",
        "description": "RECIPIENT_CHOICE: a chosen size ran out between the choice and the reservation. `data.holdReason` is `out_of_stock`. Brandmerch retries hourly and records send.reserved when stock returns; after 14 days on hold the send is cancelled (send.cancelled, reason `out_of_stock_timeout`).",
        "operationId": "webhook_send_held",
        "tags": [
          "Webhooks"
        ],
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/X-BrandMerch-Signature"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Event"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Event-Id"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Delivery-Id"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Attempt"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendHeldEvent"
              }
            }
          }
        },
        "responses": {
          "410": {
            "description": "Gone: Brandmerch stops retrying this delivery."
          },
          "2XX": {
            "description": "Delivered. Answer within 10 seconds; do the work afterwards."
          },
          "default": {
            "description": "Any other status, a redirect, a timeout or a network error is retried: 11 attempts over about 24 hours."
          }
        }
      }
    },
    "send.reserved": {
      "post": {
        "summary": "Stock is reserved and the gift is queued to ship",
        "description": "DIRECT: on create, 1 ms after send.created. RECIPIENT_CHOICE: at redemption, or when a held send is restocked.",
        "operationId": "webhook_send_reserved",
        "tags": [
          "Webhooks"
        ],
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/X-BrandMerch-Signature"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Event"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Event-Id"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Delivery-Id"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Attempt"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendReservedEvent"
              }
            }
          }
        },
        "responses": {
          "410": {
            "description": "Gone: Brandmerch stops retrying this delivery."
          },
          "2XX": {
            "description": "Delivered. Answer within 10 seconds; do the work afterwards."
          },
          "default": {
            "description": "Any other status, a redirect, a timeout or a network error is retried: 11 attempts over about 24 hours."
          }
        }
      }
    },
    "send.shipped": {
      "post": {
        "summary": "A package shipped",
        "description": "Once per package (fulfillment request): a multi-package send records one send.shipped per package. `data.status` is the send status after this package (`shipped`). A voided label that is shipped again records a new event.",
        "operationId": "webhook_send_shipped",
        "tags": [
          "Webhooks"
        ],
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/X-BrandMerch-Signature"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Event"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Event-Id"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Delivery-Id"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Attempt"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendShippedEvent"
              },
              "example": {
                "event": "send.shipped",
                "sendId": "send_3f2a9c1e-7b4d-4e0a-9c6b-2d8f1a5e7c3b",
                "data": {
                  "eventId": "5b2e7c1a-9d4f-4e8b-a3c6-0f1e2d3c4b5a",
                  "sendId": "send_3f2a9c1e-7b4d-4e0a-9c6b-2d8f1a5e7c3b",
                  "orderId": "send_3f2a9c1e-7b4d-4e0a-9c6b-2d8f1a5e7c3b",
                  "companyId": "c0a8012e-5f4b-4d3c-8a2b-1e9f8d7c6b5a",
                  "mode": "DIRECT",
                  "source": "API",
                  "status": "shipped",
                  "holdReason": null,
                  "recipient": {
                    "email": "jane@example.com",
                    "name": "Jane Smith",
                    "company": null,
                    "city": "San Francisco",
                    "state": "CA"
                  },
                  "sender": {
                    "userId": "usr_example",
                    "email": "rep@example.com",
                    "name": "Alex Rep",
                    "externalId": null,
                    "unmapped": false
                  },
                  "offer": null,
                  "items": [
                    {
                      "designId": "design_hoodie",
                      "quantity": 1,
                      "size": "M"
                    }
                  ],
                  "crm": null,
                  "metadata": null,
                  "tracking": {
                    "fulfillmentRequestId": "fr_001",
                    "trackingNumber": "1Z999AA10123456784",
                    "carrier": "UPS",
                    "trackingUrl": "https://www.ups.com/track?tracknum=1Z999AA10123456784",
                    "shippedAt": "2026-10-12T14:30:00.000Z",
                    "deliveredAt": null,
                    "carrierTrackingStatus": "IN_TRANSIT"
                  },
                  "fulfillmentRequestId": "fr_001",
                  "trackingNumber": "1Z999AA10123456784",
                  "carrier": "UPS",
                  "shippedAt": "2026-10-12T14:30:00.000Z"
                },
                "timestamp": "2026-10-12T14:31:20.000Z"
              }
            }
          }
        },
        "responses": {
          "410": {
            "description": "Gone: Brandmerch stops retrying this delivery."
          },
          "2XX": {
            "description": "Delivered. Answer within 10 seconds; do the work afterwards."
          },
          "default": {
            "description": "Any other status, a redirect, a timeout or a network error is retried: 11 attempts over about 24 hours."
          }
        }
      }
    },
    "send.delivered": {
      "post": {
        "summary": "A package was delivered",
        "description": "Once per package. `data.status` becomes `delivered` when every package is delivered; until then it stays `shipped`.",
        "operationId": "webhook_send_delivered",
        "tags": [
          "Webhooks"
        ],
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/X-BrandMerch-Signature"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Event"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Event-Id"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Delivery-Id"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Attempt"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendDeliveredEvent"
              }
            }
          }
        },
        "responses": {
          "410": {
            "description": "Gone: Brandmerch stops retrying this delivery."
          },
          "2XX": {
            "description": "Delivered. Answer within 10 seconds; do the work afterwards."
          },
          "default": {
            "description": "Any other status, a redirect, a timeout or a network error is retried: 11 attempts over about 24 hours."
          }
        }
      }
    },
    "send.exception": {
      "post": {
        "summary": "Something needs attention",
        "description": "Today: `exception.code` `out_of_stock`, recorded with send.held.",
        "operationId": "webhook_send_exception",
        "tags": [
          "Webhooks"
        ],
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/X-BrandMerch-Signature"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Event"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Event-Id"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Delivery-Id"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Attempt"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendExceptionEvent"
              }
            }
          }
        },
        "responses": {
          "410": {
            "description": "Gone: Brandmerch stops retrying this delivery."
          },
          "2XX": {
            "description": "Delivered. Answer within 10 seconds; do the work afterwards."
          },
          "default": {
            "description": "Any other status, a redirect, a timeout or a network error is retried: 11 attempts over about 24 hours."
          }
        }
      }
    },
    "send.cancelled": {
      "post": {
        "summary": "The send was cancelled",
        "description": "Through POST /api/v1/sends/{sendId}/cancel (your `reason`), an order cancelled by Brandmerch (`order_cancelled`), a hold that timed out (`out_of_stock_timeout`) or a privacy erasure (`privacy_erasure`). Final.",
        "operationId": "webhook_send_cancelled",
        "tags": [
          "Webhooks"
        ],
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/X-BrandMerch-Signature"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Event"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Event-Id"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Delivery-Id"
          },
          {
            "$ref": "#/components/parameters/X-BrandMerch-Attempt"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendCancelledEvent"
              }
            }
          }
        },
        "responses": {
          "410": {
            "description": "Gone: Brandmerch stops retrying this delivery."
          },
          "2XX": {
            "description": "Delivered. Answer within 10 seconds; do the work afterwards."
          },
          "default": {
            "description": "Any other status, a redirect, a timeout or a network error is retried: 11 attempts over about 24 hours."
          }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "X-BrandMerch-Signature": {
        "name": "X-BrandMerch-Signature",
        "in": "header",
        "required": true,
        "description": "Hex HMAC-SHA256 of the raw request body, keyed with the subscription secret. Verify over the raw bytes.",
        "schema": {
          "type": "string",
          "pattern": "^[0-9a-f]{64}$"
        }
      },
      "X-BrandMerch-Event": {
        "name": "X-BrandMerch-Event",
        "in": "header",
        "required": true,
        "description": "The event type.",
        "schema": {
          "$ref": "#/components/schemas/SendEventType"
        }
      },
      "X-BrandMerch-Event-Id": {
        "name": "X-BrandMerch-Event-Id",
        "in": "header",
        "required": true,
        "description": "The event id (equals `data.eventId`), the same on every retry and every subscription. Dedupe on it.",
        "schema": {
          "type": "string"
        }
      },
      "X-BrandMerch-Delivery-Id": {
        "name": "X-BrandMerch-Delivery-Id",
        "in": "header",
        "required": true,
        "description": "This event to this subscription; the same on every retry.",
        "schema": {
          "type": "string"
        }
      },
      "X-BrandMerch-Attempt": {
        "name": "X-BrandMerch-Attempt",
        "in": "header",
        "required": true,
        "description": "The attempt number, from 1 to 11.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 11
        }
      }
    },
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "Your API key (`bm_live_...`)."
      }
    },
    "headers": {
      "X-RateLimit-Limit": {
        "schema": {
          "type": "integer"
        },
        "description": "Gifting & Distribution only: 1000."
      },
      "X-RateLimit-Remaining": {
        "schema": {
          "type": "integer"
        },
        "description": "Gifting & Distribution only: sends left in the rolling 24-hour window."
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "The `X-API-Key` header is missing, the key does not start with `bm_live_`, or the key is unknown or revoked. No `code`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "Invalid or revoked API key"
            }
          }
        }
      },
      "Forbidden": {
        "description": "The key is valid but the account is deactivated, or its plan has no API access (the body carries `upgrade_url`). No `code`.",
        "content": {
          "application/json": {
            "schema": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/Error"
                },
                {
                  "properties": {
                    "upgrade_url": {
                      "type": "string",
                      "format": "uri"
                    }
                  }
                }
              ]
            },
            "example": {
              "error": "API access requires a Gifting & Distribution or Business subscription",
              "upgrade_url": "https://brandmerch.com/pricing"
            }
          }
        }
      },
      "IpRateLimited": {
        "description": "`rate_limited`: this IP address had 1,000 failed (4xx or 5xx) responses in 15 minutes. Wait for `Retry-After`, and fix the requests that fail.",
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer"
            },
            "description": "Seconds until the window resets (equals `retryAfterSeconds`)."
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/IpRateLimitExceeded"
            },
            "example": {
              "error": "Rate limit exceeded",
              "code": "rate_limited",
              "message": "Too many failed requests from this IP address: at most 1000 failed (4xx or 5xx) responses in 15 minutes, and successful requests do not count. Fix the requests that fail, then retry after the number of seconds in Retry-After.",
              "limit": 1000,
              "remaining": 0,
              "reset": 1791547200,
              "retryAfterSeconds": 412
            }
          }
        }
      }
    },
    "schemas": {
      "SendStatus": {
        "type": "string",
        "enum": [
          "created",
          "awaiting_redemption",
          "redeemed",
          "held",
          "reserved",
          "shipped",
          "delivered",
          "cancelled",
          "expired",
          "declined"
        ],
        "description": "The send lifecycle.\n\n- `created`: recorded; never returned at rest for a send created through this API (a DIRECT send is `reserved` from its first moment, a RECIPIENT_CHOICE send is `awaiting_redemption`). An API order created before sends existed, with no packages, reads `created`.\n- `awaiting_redemption`: RECIPIENT_CHOICE; the invitation is live and the recipient has not chosen yet.\n- `redeemed`: the recipient chose; transient, the same transaction moves the send to `reserved` or `held`.\n- `held`: the choice could not be reserved (`holdReason` `out_of_stock`); retried hourly, cancelled after 14 days.\n- `reserved`: stock is reserved and the packages are waiting to ship, or being picked and packed.\n- `shipped`: at least one package has shipped.\n- `delivered`: every package is delivered (carrier tracking or a warehouse mark).\n- `cancelled`, `expired`, `declined`: closed. `expired` reopens only through a resend."
      },
      "SendMode": {
        "type": "string",
        "enum": [
          "DIRECT",
          "RECIPIENT_CHOICE"
        ],
        "description": "`DIRECT`: you name the items and the address. `RECIPIENT_CHOICE`: the recipient chooses from a gift link and enters their own address."
      },
      "SendSource": {
        "type": "string",
        "enum": [
          "API",
          "HUBSPOT",
          "DASHBOARD"
        ],
        "description": "Where the send was created. Every send created through this API is `API`."
      },
      "HoldReason": {
        "type": [
          "string",
          "null"
        ],
        "enum": [
          "budget",
          "per_send_cap",
          "approval_required",
          "out_of_stock",
          null
        ],
        "description": "Why a `held` send is held; null whenever the send is not held, including after a hold ends in a reservation or a cancel. Only `out_of_stock` is used today."
      },
      "SendEventType": {
        "type": "string",
        "enum": [
          "send.created",
          "send.invited",
          "send.viewed",
          "send.redeemed",
          "send.declined",
          "send.expired",
          "send.held",
          "send.approved",
          "send.denied",
          "send.reserved",
          "send.shipped",
          "send.in_transit",
          "send.out_for_delivery",
          "send.delivered",
          "send.exception",
          "send.cancelled"
        ],
        "description": "Every event a webhook subscription can subscribe to. `send.approved`, `send.denied`, `send.in_transit` and `send.out_for_delivery` are reserved: you can subscribe to them, but they are not sent yet."
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "A human-readable message. Never contains recipient details."
          },
          "code": {
            "type": "string",
            "description": "A stable, machine-readable code. Match on `code`, not on `error`. Authentication refusals, the inventory endpoint and the daily send cap (429) answer without a code."
          }
        },
        "additionalProperties": true,
        "description": "Every non-2xx body of the Sends API. Some refusals add details next to `error` and `code` (for example `required`, `stockIssues`, `reasons`, `status`). The one exception is a request the server refuses before it reaches the API (a body over 50 MB, or an unsupported encoding or charset): HTTP 500 `{ status, code: 1000, message }`."
      },
      "CrmBlock": {
        "type": "object",
        "description": "CRM correlation ids, echoed back on the send and in every event. Values are stored as given (`provider` lower-cased).",
        "properties": {
          "provider": {
            "type": [
              "string",
              "null"
            ],
            "description": "For example `hubspot` or `salesforce`.",
            "maxLength": 32
          },
          "portalId": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200
          },
          "objectType": {
            "type": [
              "string",
              "null"
            ],
            "description": "For example `contact`, `Contact`, `deal`.",
            "maxLength": 200
          },
          "objectId": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200
          },
          "ownerId": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200
          },
          "dealId": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200
          },
          "workflowId": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200
          },
          "callbackId": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200
          }
        },
        "additionalProperties": false
      },
      "Metadata": {
        "type": "object",
        "description": "Your own flat key-value data, echoed back on the send and in every event. At most 50 keys; keys are 1-40 characters of letters, digits, `_`, `.` and `-`; values are strings up to 500 characters, numbers, booleans or null.",
        "maxProperties": 50,
        "propertyNames": {
          "pattern": "^[A-Za-z0-9_.-]{1,40}$"
        },
        "additionalProperties": {
          "oneOf": [
            {
              "type": "string",
              "maxLength": 500
            },
            {
              "type": "number"
            },
            {
              "type": "boolean"
            },
            {
              "type": "null"
            }
          ]
        }
      },
      "SenderInput": {
        "type": "object",
        "description": "Who the gift is from. Name a user by `userId` or `email` (userId wins when both are given). A named sender must be an active member of your company who may send gifts: an owner, a manager, a direct creative, or a member granted \"Can send gifts\" on the Members page. Omit both to attribute the send to the company default (the first active owner, then manager). `externalId` is stored and echoed; it does not name a user. Unknown keys are refused.",
        "properties": {
          "userId": {
            "type": "string",
            "maxLength": 64
          },
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 254
          },
          "externalId": {
            "type": "string",
            "maxLength": 200
          }
        },
        "additionalProperties": false
      },
      "SendItemInput": {
        "type": "object",
        "required": [
          "designId",
          "quantity"
        ],
        "properties": {
          "designId": {
            "type": "string",
            "minLength": 1,
            "description": "A design id from `GET /api/v1/sends/inventory`."
          },
          "quantity": {
            "type": "integer",
            "minimum": 1,
            "maximum": 1000
          },
          "size": {
            "type": "string",
            "minLength": 1,
            "description": "For a sized design, for example `M`. Omit for a one-size design."
          }
        }
      },
      "DirectRecipient": {
        "type": "object",
        "required": [
          "firstName",
          "lastName",
          "address1",
          "city",
          "state",
          "zipCode"
        ],
        "properties": {
          "firstName": {
            "type": "string",
            "minLength": 1
          },
          "lastName": {
            "type": "string",
            "minLength": 1
          },
          "company": {
            "type": "string"
          },
          "address1": {
            "type": "string",
            "minLength": 1
          },
          "address2": {
            "type": "string"
          },
          "city": {
            "type": "string",
            "minLength": 1
          },
          "state": {
            "type": "string",
            "minLength": 1
          },
          "zipCode": {
            "type": "string",
            "minLength": 1
          },
          "country": {
            "type": "string",
            "description": "ISO 3166-1 alpha-2, for example `US` or `CA`. Common country names are normalized. Omitted or blank means `US`."
          },
          "phone": {
            "type": "string"
          },
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 254,
            "description": "Optional. When given, Brandmerch emails the recipient carrier tracking when each package ships and again when the send is delivered. Omit it to send no email."
          }
        }
      },
      "RecipientChoiceRecipient": {
        "type": "object",
        "required": [
          "email"
        ],
        "description": "The person invited to choose. Address fields are ignored: the recipient enters their address when they redeem.",
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 254,
            "description": "Trimmed and lower-cased. The gift's confirmation and shipping mail go to this address."
          },
          "name": {
            "type": "string",
            "description": "Shown to the recipient. When absent, `firstName` and `lastName` are joined."
          },
          "firstName": {
            "type": "string"
          },
          "lastName": {
            "type": "string"
          },
          "company": {
            "type": "string"
          }
        }
      },
      "CreateDirectSendRequest": {
        "type": "object",
        "title": "DIRECT",
        "required": [
          "items",
          "recipient"
        ],
        "properties": {
          "mode": {
            "const": "DIRECT",
            "description": "Optional; DIRECT is the default."
          },
          "items": {
            "type": "array",
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/SendItemInput"
            },
            "description": "Lines with the same designId and size are combined. At most 25 lines after combining, and 1,000 units per line."
          },
          "recipient": {
            "$ref": "#/components/schemas/DirectRecipient"
          },
          "sender": {
            "$ref": "#/components/schemas/SenderInput"
          },
          "crm": {
            "$ref": "#/components/schemas/CrmBlock"
          },
          "metadata": {
            "$ref": "#/components/schemas/Metadata"
          },
          "giftNote": {
            "type": "string",
            "maxLength": 500,
            "description": "Printed and packed with the gift."
          },
          "giftBox": {
            "type": "boolean",
            "description": "Pack in a gift box."
          },
          "notify": {
            "const": false,
            "description": "Brandmerch does not send invitations; `true` is refused with `notify_unsupported`."
          },
          "idempotencyKey": {
            "type": "string",
            "maxLength": 200,
            "description": "See Idempotency."
          }
        }
      },
      "CreateRecipientChoiceSendRequest": {
        "type": "object",
        "title": "RECIPIENT_CHOICE",
        "required": [
          "mode",
          "offer",
          "recipient"
        ],
        "properties": {
          "mode": {
            "const": "RECIPIENT_CHOICE"
          },
          "offer": {
            "type": "object",
            "required": [
              "giftLinkId"
            ],
            "properties": {
              "giftLinkId": {
                "type": "string",
                "description": "A gift link of your company with personal invitation links (MAGIC_LINK), status OPEN, a collection, and fulfillment mode ON_SUBMIT (\"Ship each gift when the recipient submits\")."
              }
            }
          },
          "recipient": {
            "$ref": "#/components/schemas/RecipientChoiceRecipient"
          },
          "expiresInDays": {
            "type": "integer",
            "minimum": 1,
            "maximum": 365,
            "description": "How long the invitation stays valid. Default: the gift link's invitation expiry (30 days unless changed on the link)."
          },
          "sender": {
            "$ref": "#/components/schemas/SenderInput"
          },
          "crm": {
            "$ref": "#/components/schemas/CrmBlock"
          },
          "metadata": {
            "$ref": "#/components/schemas/Metadata"
          },
          "giftNote": {
            "type": "string",
            "maxLength": 500
          },
          "giftBox": {
            "type": "boolean"
          },
          "notify": {
            "const": false,
            "description": "Brandmerch does not send invitations: deliver `redemptionUrl` from your CRM or email tool. `true` is refused with `notify_unsupported`."
          },
          "idempotencyKey": {
            "type": "string",
            "maxLength": 200
          },
          "items": {
            "type": "null",
            "description": "Omit: the recipient chooses. Items are refused with `items_not_allowed`."
          }
        }
      },
      "CreateSendRequest": {
        "oneOf": [
          {
            "$ref": "#/components/schemas/CreateDirectSendRequest"
          },
          {
            "$ref": "#/components/schemas/CreateRecipientChoiceSendRequest"
          }
        ],
        "description": "A DIRECT body (mode omitted or `DIRECT`) or a RECIPIENT_CHOICE body. `offer` and `expiresInDays` are refused on a DIRECT body; `items` is refused on a RECIPIENT_CHOICE body."
      },
      "CreateSendResponse": {
        "type": "object",
        "required": [
          "sendId",
          "orderId",
          "status",
          "mode",
          "createdAt"
        ],
        "properties": {
          "sendId": {
            "type": "string"
          },
          "orderId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Equals sendId once an order exists; null while a RECIPIENT_CHOICE send awaits redemption."
          },
          "status": {
            "$ref": "#/components/schemas/SendStatus"
          },
          "mode": {
            "$ref": "#/components/schemas/SendMode"
          },
          "fulfillmentRequests": {
            "type": "integer",
            "description": "DIRECT on create: the number of packages reserved."
          },
          "redemptionUrl": {
            "type": "string",
            "format": "uri",
            "description": "RECIPIENT_CHOICE: the recipient's personal link (`https://brandmerch.com/g/<slug>?t=<token>`). Treat it as a secret: whoever holds it can choose the gift. On a replay it is present only while the send awaits redemption with a live invitation."
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time",
            "description": "RECIPIENT_CHOICE: when the invitation expires."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "idempotent": {
            "const": true,
            "description": "Present on a replay (HTTP 200) only."
          }
        }
      },
      "SendRecipientView": {
        "type": "object",
        "required": [
          "email",
          "name",
          "firstName",
          "lastName",
          "company",
          "city",
          "state",
          "country"
        ],
        "description": "The street address is never returned. For RECIPIENT_CHOICE, city, state and country come from the address entered at redemption.",
        "properties": {
          "email": {
            "type": [
              "string",
              "null"
            ]
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "firstName": {
            "type": [
              "string",
              "null"
            ]
          },
          "lastName": {
            "type": [
              "string",
              "null"
            ]
          },
          "company": {
            "type": [
              "string",
              "null"
            ]
          },
          "city": {
            "type": [
              "string",
              "null"
            ]
          },
          "state": {
            "type": [
              "string",
              "null"
            ]
          },
          "country": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "SenderView": {
        "type": "object",
        "required": [
          "userId",
          "email",
          "name",
          "externalId",
          "unmapped"
        ],
        "properties": {
          "userId": {
            "type": [
              "string",
              "null"
            ]
          },
          "email": {
            "type": [
              "string",
              "null"
            ]
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "externalId": {
            "type": [
              "string",
              "null"
            ]
          },
          "unmapped": {
            "type": "boolean",
            "description": "Reserved for CRM user mapping; always false today."
          }
        }
      },
      "OfferView": {
        "type": [
          "object",
          "null"
        ],
        "required": [
          "giftLinkId",
          "title"
        ],
        "properties": {
          "giftLinkId": {
            "type": "string"
          },
          "title": {
            "type": [
              "string",
              "null"
            ],
            "description": "The gift link title when the send was created."
          }
        }
      },
      "PackageView": {
        "type": "object",
        "required": [
          "id",
          "status",
          "trackingNumber",
          "carrier",
          "trackingUrl",
          "shippedAt",
          "deliveredAt",
          "carrierTrackingStatus",
          "labelUrl"
        ],
        "description": "One package (fulfillment request).",
        "properties": {
          "id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "description": "Warehouse status, upper case: PENDING, RESERVED, PICKED, PACKED, SHIPPED, DELIVERED or CANCELLED."
          },
          "trackingNumber": {
            "type": [
              "string",
              "null"
            ]
          },
          "carrier": {
            "type": [
              "string",
              "null"
            ]
          },
          "trackingUrl": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "shippedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "deliveredAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "carrierTrackingStatus": {
            "type": [
              "string",
              "null"
            ]
          },
          "labelUrl": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "SendView": {
        "type": "object",
        "required": [
          "sendId",
          "orderId",
          "mode",
          "source",
          "status",
          "holdReason",
          "legacy",
          "recipient",
          "sender",
          "offer",
          "redemptionUrl",
          "expiresAt",
          "items",
          "fulfillment",
          "giftNote",
          "giftBox",
          "crm",
          "metadata",
          "fees",
          "timeline",
          "createdAt",
          "updatedAt"
        ],
        "properties": {
          "sendId": {
            "type": "string"
          },
          "orderId": {
            "type": [
              "string",
              "null"
            ]
          },
          "mode": {
            "$ref": "#/components/schemas/SendMode"
          },
          "source": {
            "$ref": "#/components/schemas/SendSource"
          },
          "status": {
            "$ref": "#/components/schemas/SendStatus"
          },
          "holdReason": {
            "$ref": "#/components/schemas/HoldReason"
          },
          "legacy": {
            "type": "boolean",
            "description": "true for an API order created before sends existed (read by id only, never listed)."
          },
          "recipient": {
            "$ref": "#/components/schemas/SendRecipientView"
          },
          "sender": {
            "$ref": "#/components/schemas/SenderView"
          },
          "offer": {
            "$ref": "#/components/schemas/OfferView"
          },
          "redemptionUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "Only while the send awaits redemption with an unused, unexpired invitation.",
            "format": "uri"
          },
          "expiresAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "RECIPIENT_CHOICE: when the current invitation expires."
          },
          "items": {
            "type": "array",
            "description": "DIRECT: the lines you sent. RECIPIENT_CHOICE: empty until the recipient chooses, then the recipient's choice.",
            "items": {
              "type": "object",
              "required": [
                "designId",
                "printConfigId",
                "quantity",
                "size"
              ],
              "properties": {
                "designId": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "printConfigId": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "quantity": {
                  "type": "integer"
                },
                "size": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              }
            }
          },
          "fulfillment": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PackageView"
            },
            "description": "Live packages (removed ones are left out)."
          },
          "giftNote": {
            "type": [
              "string",
              "null"
            ]
          },
          "giftBox": {
            "type": "boolean"
          },
          "crm": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/CrmBlock"
              },
              {
                "type": "null"
              }
            ]
          },
          "metadata": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true
          },
          "fees": {
            "type": "object",
            "required": [
              "pickPackCents",
              "shippingCents"
            ],
            "properties": {
              "pickPackCents": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "Pick & pack plus gift add-ons (gift note, gift box) booked when the send's first package ships, in cents. null until then, and for sends created before your pick & pack billing start date; 0 when your contract waives the fee."
              },
              "shippingCents": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "The packages' shipping cost in cents, counted once per shipping label: a package holding several items ships under one label and its cost is counted once, not once per item. null while no package has a cost."
              }
            }
          },
          "timeline": {
            "type": "object",
            "required": [
              "createdAt",
              "viewedAt",
              "redeemedAt",
              "reservedAt",
              "shippedAt",
              "deliveredAt",
              "declinedAt",
              "expiredAt",
              "cancelledAt"
            ],
            "properties": {
              "createdAt": {
                "type": "string",
                "format": "date-time"
              },
              "viewedAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time",
                "description": "First view of the current invitation (cleared by a resend)."
              },
              "redeemedAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "reservedAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "shippedAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "deliveredAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "declinedAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "expiredAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "cancelledAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              }
            }
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SendList": {
        "type": "object",
        "required": [
          "data",
          "nextCursor"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SendView"
            }
          },
          "nextCursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Pass as `cursor` for the next page; null on the last page."
          }
        }
      },
      "CancelSendRequest": {
        "type": "object",
        "properties": {
          "reason": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 500,
            "description": "Carried in the send.cancelled event as `data.reason`."
          }
        }
      },
      "CancelSendResponse": {
        "type": "object",
        "required": [
          "sendId",
          "status",
          "cancelledAt"
        ],
        "properties": {
          "sendId": {
            "type": "string"
          },
          "status": {
            "const": "cancelled"
          },
          "cancelledAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ResendRequest": {
        "type": "object",
        "properties": {
          "expiresInDays": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "maximum": 365,
            "description": "How long the new invitation stays valid. Absent or null: the gift link's invitation expiry."
          }
        }
      },
      "ResendResponse": {
        "type": "object",
        "required": [
          "sendId",
          "status",
          "redemptionUrl",
          "expiresAt",
          "resendCount"
        ],
        "properties": {
          "sendId": {
            "type": "string"
          },
          "status": {
            "const": "awaiting_redemption"
          },
          "redemptionUrl": {
            "type": "string",
            "format": "uri",
            "description": "The NEW link. The previous link stops working."
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          },
          "resendCount": {
            "type": "integer",
            "minimum": 1
          }
        }
      },
      "InventoryDesign": {
        "type": "object",
        "required": [
          "designId",
          "title",
          "sku",
          "produced",
          "quantityAvailable",
          "quantityOnHand",
          "quantityReserved"
        ],
        "properties": {
          "designId": {
            "type": "string"
          },
          "title": {
            "type": [
              "string",
              "null"
            ]
          },
          "sku": {
            "type": [
              "string",
              "null"
            ]
          },
          "produced": {
            "type": "boolean",
            "description": "false: the design cannot be sent yet (see notReadyReason)."
          },
          "quantityAvailable": {
            "type": "integer",
            "description": "On hand minus reserved, across sizes."
          },
          "quantityOnHand": {
            "type": "integer"
          },
          "quantityReserved": {
            "type": "integer"
          },
          "notReadyReason": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "NOT_PRODUCED",
              "PRINT_CONFIG_DELETED",
              "NOT_ACCESSIBLE",
              null
            ],
            "description": "Collection lookups only."
          }
        }
      },
      "InventoryByCollection": {
        "type": "object",
        "required": [
          "collectionId",
          "collectionTitle",
          "designs"
        ],
        "properties": {
          "collectionId": {
            "type": "string"
          },
          "collectionTitle": {
            "type": [
              "string",
              "null"
            ]
          },
          "designs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/InventoryDesign"
            }
          }
        }
      },
      "InventoryByDesigns": {
        "type": "object",
        "required": [
          "designs",
          "unresolved"
        ],
        "properties": {
          "designs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/InventoryDesign"
            }
          },
          "unresolved": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "designId",
                "reason"
              ],
              "properties": {
                "designId": {
                  "type": "string"
                },
                "reason": {
                  "type": "string",
                  "enum": [
                    "NOT_FOUND",
                    "NOT_PRODUCED",
                    "PRINT_CONFIG_DELETED",
                    "NOT_ACCESSIBLE",
                    "OUT_OF_KEY_SCOPE"
                  ]
                }
              }
            }
          }
        }
      },
      "StockIssue": {
        "type": "object",
        "required": [
          "designId",
          "size",
          "requested",
          "available"
        ],
        "properties": {
          "designId": {
            "type": "string"
          },
          "size": {
            "type": [
              "string",
              "null"
            ]
          },
          "requested": {
            "type": "integer"
          },
          "available": {
            "type": "integer"
          }
        }
      },
      "OfferNotReadyReason": {
        "type": "string",
        "enum": [
          "access_mode_not_magic_link",
          "status_not_open",
          "no_collection",
          "fulfillment_mode_not_on_submit"
        ]
      },
      "DailyLimitExceeded": {
        "type": "object",
        "required": [
          "error",
          "message",
          "limit",
          "remaining",
          "reset",
          "retryAfterSeconds",
          "upgrade_url"
        ],
        "properties": {
          "error": {
            "const": "Rate limit exceeded"
          },
          "message": {
            "type": "string"
          },
          "limit": {
            "type": "integer",
            "const": 1000
          },
          "remaining": {
            "type": "integer",
            "const": 0
          },
          "reset": {
            "type": "integer",
            "description": "Unix seconds when the oldest send in the window ages out."
          },
          "retryAfterSeconds": {
            "type": "integer"
          },
          "upgrade_url": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "PrivacyDeleteRequest": {
        "type": "object",
        "required": [
          "email"
        ],
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 320,
            "description": "The recipient's email address. Matched case-insensitively, within your company only."
          }
        }
      },
      "PrivacyDeleteQueued": {
        "type": "object",
        "required": [
          "erasureId",
          "status",
          "requestedAt"
        ],
        "properties": {
          "erasureId": {
            "type": "string"
          },
          "status": {
            "$ref": "#/components/schemas/PiiErasureStatus"
          },
          "requestedAt": {
            "type": "string",
            "format": "date-time"
          },
          "existing": {
            "const": true,
            "description": "Present (HTTP 200) when this address already had an open request; that request is returned."
          }
        }
      },
      "PiiErasureStatus": {
        "type": "string",
        "enum": [
          "pending",
          "running",
          "completed",
          "completed_with_exceptions",
          "failed"
        ],
        "description": "`pending` (queued; a follow-up waits about 24 hours before it runs), `running`, then one of the final statuses: `completed`, `completed_with_exceptions` (see `exceptionsByReason`) or `failed` (the erasure did not finish after three attempts; contact support)."
      },
      "PiiErasureView": {
        "type": "object",
        "required": [
          "erasureId",
          "status",
          "requestedAt",
          "completedAt",
          "tablesTouched",
          "exceptionsCount",
          "exceptionsByReason",
          "s3KeysDeletedCount"
        ],
        "description": "The request and its counts. The email address is never returned.",
        "properties": {
          "erasureId": {
            "type": "string"
          },
          "status": {
            "$ref": "#/components/schemas/PiiErasureStatus"
          },
          "requestedAt": {
            "type": "string",
            "format": "date-time"
          },
          "completedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "tablesTouched": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": {
              "type": "integer"
            },
            "description": "Rows changed per data store, once processed."
          },
          "exceptionsCount": {
            "type": "integer",
            "description": "Items this run could not erase. Their reasons are in `exceptionsByReason`."
          },
          "exceptionsByReason": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            },
            "description": "The exceptions counted per reason (never row ids); empty until the run finishes. `in_fulfillment`: a package was being picked or packed and kept its address so its label could print; a follow-up request for the same address runs about 24 hours later (see POST /api/v1/sends/privacy-delete). Other reasons, such as `shared_label` or `split_csv_file`, are not re-run automatically; contact support if one matters to you."
          },
          "s3KeysDeletedCount": {
            "type": "integer",
            "description": "Stored files (shipping labels, packing slips) deleted."
          }
        }
      },
      "SendEventRecipient": {
        "type": "object",
        "required": [
          "email",
          "name",
          "company",
          "city",
          "state"
        ],
        "properties": {
          "email": {
            "type": [
              "string",
              "null"
            ]
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "company": {
            "type": [
              "string",
              "null"
            ]
          },
          "city": {
            "type": [
              "string",
              "null"
            ]
          },
          "state": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "SendEventTracking": {
        "type": [
          "object",
          "null"
        ],
        "required": [
          "fulfillmentRequestId",
          "trackingNumber",
          "carrier",
          "trackingUrl",
          "shippedAt",
          "deliveredAt",
          "carrierTrackingStatus"
        ],
        "description": "The package a per-package event is about; null on every other event.",
        "properties": {
          "fulfillmentRequestId": {
            "type": "string"
          },
          "trackingNumber": {
            "type": [
              "string",
              "null"
            ]
          },
          "carrier": {
            "type": [
              "string",
              "null"
            ]
          },
          "trackingUrl": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "shippedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "deliveredAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "carrierTrackingStatus": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "SendEventData": {
        "type": "object",
        "description": "The `data` object of every event. It is frozen when the event is recorded, so every retry carries identical bytes. Every key below is always present (null rather than absent); events add the keys listed on their own schema.",
        "required": [
          "eventId",
          "sendId",
          "orderId",
          "companyId",
          "mode",
          "source",
          "status",
          "holdReason",
          "recipient",
          "sender",
          "offer",
          "items",
          "crm",
          "metadata",
          "tracking"
        ],
        "properties": {
          "eventId": {
            "type": "string",
            "description": "Equals the X-BrandMerch-Event-Id header. Dedupe on it."
          },
          "sendId": {
            "type": "string"
          },
          "orderId": {
            "type": [
              "string",
              "null"
            ]
          },
          "companyId": {
            "type": "string"
          },
          "mode": {
            "$ref": "#/components/schemas/SendMode"
          },
          "source": {
            "$ref": "#/components/schemas/SendSource"
          },
          "status": {
            "allOf": [
              {
                "$ref": "#/components/schemas/SendStatus"
              }
            ],
            "description": "The send status after this event."
          },
          "holdReason": {
            "$ref": "#/components/schemas/HoldReason"
          },
          "recipient": {
            "$ref": "#/components/schemas/SendEventRecipient"
          },
          "sender": {
            "$ref": "#/components/schemas/SenderView"
          },
          "offer": {
            "$ref": "#/components/schemas/OfferView"
          },
          "items": {
            "type": "array",
            "description": "[] until a RECIPIENT_CHOICE recipient chooses.",
            "items": {
              "type": "object",
              "required": [
                "designId",
                "quantity",
                "size"
              ],
              "properties": {
                "designId": {
                  "type": "string"
                },
                "quantity": {
                  "type": "integer"
                },
                "size": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              }
            }
          },
          "crm": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/CrmBlock"
              },
              {
                "type": "null"
              }
            ]
          },
          "metadata": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true
          },
          "tracking": {
            "$ref": "#/components/schemas/SendEventTracking"
          },
          "piiStripped": {
            "const": true,
            "description": "Present when the recipient's details were erased before this delivery: recipient email, name and company, metadata and redemptionUrl are null."
          }
        },
        "additionalProperties": true
      },
      "WebhookEnvelope": {
        "type": "object",
        "required": [
          "event",
          "sendId",
          "data",
          "timestamp"
        ],
        "description": "The body of every webhook delivery, keys in this order.",
        "properties": {
          "event": {
            "$ref": "#/components/schemas/SendEventType"
          },
          "sendId": {
            "type": "string"
          },
          "data": {
            "$ref": "#/components/schemas/SendEventData"
          },
          "timestamp": {
            "type": "string",
            "format": "date-time",
            "description": "When the event was recorded (not when it was delivered). Stable across retries; events recorded together are 1 ms apart."
          }
        }
      },
      "SendCreatedEvent": {
        "description": "Every send, once. DIRECT: `data.status` is `reserved` (the order and its reservation commit together) and `fulfillmentRequests` counts the packages. RECIPIENT_CHOICE: `data.status` is `awaiting_redemption` and `fulfillmentRequests` is 0.",
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookEnvelope"
          },
          {
            "type": "object",
            "properties": {
              "event": {
                "const": "send.created"
              },
              "data": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/SendEventData"
                  },
                  {
                    "type": "object",
                    "required": [
                      "fulfillmentRequests"
                    ],
                    "properties": {
                      "fulfillmentRequests": {
                        "type": "integer",
                        "minimum": 0
                      }
                    }
                  }
                ]
              }
            }
          }
        ]
      },
      "SendInvitedEvent": {
        "description": "RECIPIENT_CHOICE: on create (1 ms after send.created) and on every resend. Carries the redemption URL to deliver to the recipient; a resend makes the previous URL stop working.",
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookEnvelope"
          },
          {
            "type": "object",
            "properties": {
              "event": {
                "const": "send.invited"
              },
              "data": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/SendEventData"
                  },
                  {
                    "type": "object",
                    "required": [
                      "redemptionUrl",
                      "expiresAt",
                      "resendCount"
                    ],
                    "properties": {
                      "redemptionUrl": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "The live redemption URL. Treat it as a secret.",
                        "format": "uri"
                      },
                      "expiresAt": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "date-time"
                      },
                      "resendCount": {
                        "type": "integer",
                        "minimum": 0
                      }
                    }
                  }
                ]
              }
            }
          }
        ]
      },
      "SendViewedEvent": {
        "description": "The first time the recipient opens an invitation. A resend issues a new invitation, which records its own send.viewed.",
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookEnvelope"
          },
          {
            "type": "object",
            "properties": {
              "event": {
                "const": "send.viewed"
              },
              "data": {
                "$ref": "#/components/schemas/SendEventData"
              }
            }
          }
        ]
      },
      "SendRedeemedEvent": {
        "description": "RECIPIENT_CHOICE. `data.items` is the recipient's choice; `data.status` is `redeemed`. Followed by send.reserved, or by send.held and send.exception.",
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookEnvelope"
          },
          {
            "type": "object",
            "properties": {
              "event": {
                "const": "send.redeemed"
              },
              "data": {
                "$ref": "#/components/schemas/SendEventData"
              }
            }
          }
        ]
      },
      "SendDeclinedEvent": {
        "description": "RECIPIENT_CHOICE. Final.",
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookEnvelope"
          },
          {
            "type": "object",
            "properties": {
              "event": {
                "const": "send.declined"
              },
              "data": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/SendEventData"
                  },
                  {
                    "type": "object",
                    "required": [
                      "reason"
                    ],
                    "properties": {
                      "reason": {
                        "type": "null"
                      }
                    }
                  }
                ]
              }
            }
          }
        ]
      },
      "SendExpiredEvent": {
        "description": "RECIPIENT_CHOICE. Recorded by an hourly job after `expiresAt`. A resend reopens the send.",
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookEnvelope"
          },
          {
            "type": "object",
            "properties": {
              "event": {
                "const": "send.expired"
              },
              "data": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/SendEventData"
                  },
                  {
                    "type": "object",
                    "required": [
                      "expiresAt"
                    ],
                    "properties": {
                      "expiresAt": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "date-time"
                      }
                    }
                  }
                ]
              }
            }
          }
        ]
      },
      "SendHeldEvent": {
        "description": "RECIPIENT_CHOICE: a chosen size ran out between the choice and the reservation. `data.holdReason` is `out_of_stock`. Brandmerch retries hourly and records send.reserved when stock returns; after 14 days on hold the send is cancelled (send.cancelled, reason `out_of_stock_timeout`).",
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookEnvelope"
          },
          {
            "type": "object",
            "properties": {
              "event": {
                "const": "send.held"
              },
              "data": {
                "$ref": "#/components/schemas/SendEventData"
              }
            }
          }
        ]
      },
      "SendReservedEvent": {
        "description": "DIRECT: on create, 1 ms after send.created. RECIPIENT_CHOICE: at redemption, or when a held send is restocked.",
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookEnvelope"
          },
          {
            "type": "object",
            "properties": {
              "event": {
                "const": "send.reserved"
              },
              "data": {
                "$ref": "#/components/schemas/SendEventData"
              }
            }
          }
        ]
      },
      "SendShippedEvent": {
        "description": "Once per package (fulfillment request): a multi-package send records one send.shipped per package. `data.status` is the send status after this package (`shipped`). A voided label that is shipped again records a new event.",
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookEnvelope"
          },
          {
            "type": "object",
            "properties": {
              "event": {
                "const": "send.shipped"
              },
              "data": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/SendEventData"
                  },
                  {
                    "type": "object",
                    "required": [
                      "fulfillmentRequestId",
                      "trackingNumber",
                      "carrier",
                      "shippedAt"
                    ],
                    "properties": {
                      "fulfillmentRequestId": {
                        "type": "string"
                      },
                      "trackingNumber": {
                        "type": [
                          "string",
                          "null"
                        ]
                      },
                      "carrier": {
                        "type": [
                          "string",
                          "null"
                        ]
                      },
                      "shippedAt": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "date-time"
                      }
                    }
                  }
                ]
              }
            }
          }
        ]
      },
      "SendDeliveredEvent": {
        "description": "Once per package. `data.status` becomes `delivered` when every package is delivered; until then it stays `shipped`.",
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookEnvelope"
          },
          {
            "type": "object",
            "properties": {
              "event": {
                "const": "send.delivered"
              },
              "data": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/SendEventData"
                  },
                  {
                    "type": "object",
                    "required": [
                      "fulfillmentRequestId",
                      "trackingNumber",
                      "carrier",
                      "deliveredAt"
                    ],
                    "properties": {
                      "fulfillmentRequestId": {
                        "type": "string"
                      },
                      "trackingNumber": {
                        "type": [
                          "string",
                          "null"
                        ]
                      },
                      "carrier": {
                        "type": [
                          "string",
                          "null"
                        ]
                      },
                      "deliveredAt": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "date-time"
                      }
                    }
                  }
                ]
              }
            }
          }
        ]
      },
      "SendExceptionEvent": {
        "description": "Today: `exception.code` `out_of_stock`, recorded with send.held.",
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookEnvelope"
          },
          {
            "type": "object",
            "properties": {
              "event": {
                "const": "send.exception"
              },
              "data": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/SendEventData"
                  },
                  {
                    "type": "object",
                    "required": [
                      "exception"
                    ],
                    "properties": {
                      "exception": {
                        "type": [
                          "object",
                          "null"
                        ],
                        "required": [
                          "code",
                          "message"
                        ],
                        "properties": {
                          "code": {
                            "type": "string",
                            "examples": [
                              "out_of_stock"
                            ]
                          },
                          "message": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                ]
              }
            }
          }
        ]
      },
      "SendCancelledEvent": {
        "description": "Through POST /api/v1/sends/{sendId}/cancel (your `reason`), an order cancelled by Brandmerch (`order_cancelled`), a hold that timed out (`out_of_stock_timeout`) or a privacy erasure (`privacy_erasure`). Final.",
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookEnvelope"
          },
          {
            "type": "object",
            "properties": {
              "event": {
                "const": "send.cancelled"
              },
              "data": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/SendEventData"
                  },
                  {
                    "type": "object",
                    "required": [
                      "reason"
                    ],
                    "properties": {
                      "reason": {
                        "type": [
                          "string",
                          "null"
                        ]
                      }
                    }
                  }
                ]
              }
            }
          }
        ]
      },
      "IpRateLimitExceeded": {
        "type": "object",
        "required": [
          "error",
          "code",
          "message",
          "limit",
          "remaining",
          "reset",
          "retryAfterSeconds"
        ],
        "description": "The per-IP failure limit: at most 1,000 failed (4xx or 5xx) responses per 15 minutes per IP address. Successful requests do not count, and a plan upgrade does not lift it.",
        "properties": {
          "error": {
            "const": "Rate limit exceeded"
          },
          "code": {
            "const": "rate_limited"
          },
          "message": {
            "type": "string"
          },
          "limit": {
            "type": "integer"
          },
          "remaining": {
            "type": "integer",
            "const": 0
          },
          "reset": {
            "type": "integer",
            "description": "Unix seconds when this address's window resets."
          },
          "retryAfterSeconds": {
            "type": "integer",
            "minimum": 1
          }
        }
      }
    }
  }
}
