---
title: "Add one person to an active playbook"
method: POST
path: "/playbooks/{id}/people"
tags: ["Playbooks"]
---

# Add one person to an active playbook

`POST /playbooks/{id}/people`

## Request

- Base URL: `https://gtmswag.com/api/v1`
- URL: `https://gtmswag.com/api/v1/playbooks/{id}/people`
- Auth: HTTP bearer

Call once per trigger (new role, signup, closed deal). The playbook stays on; no `policyVersion` needed. Read `approval` in the result:

- `waiting` with a `reason`: the person waits in the approval queue; nothing is charged or sent. Hand `approvalUrl` to a person.
- `auto_approved` with `sendId`: the playbook's auto-approve rule (set by a signed-in owner or admin) approved the gift within its per-gift limit and monthly budget and paid it from the balance.
- `scheduled`: the rule finishes it in the background; watch `GET /events` for `send.auto_approved`.
- `approved` (a person already approved; `sendId`) or `skipped` (a person skipped them).

Idempotent on `recipient.sourceSystem` + `recipient.externalId` (else `recipientId`, then a unique email): a repeat returns `alreadyQueued:true` and never approves or charges twice. A draft or paused playbook refuses with 409 PLAYBOOK_OFF; a batch campaign with 409 NOT_ALWAYS_ON; concurrent adds to one playbook may return 409 PLAYBOOK_BUSY with a short `retryAfterMs` (send the same call again).

## Path parameters

- `id` string, required

## Request body

- object
  - `recipient` RecipientInput, required
    - `name` string: Recipient's full name.
    - `recipientId` string: Stable workspace recipient ID; never infer from a name.
    - `sourceSystem` string: Original source system name; pair with externalId for stable matching.
    - `profileUrl` string, uri: Supplied personal profile URL; photo sourcing requires separate permission.
    - `importId` string: Stable customer import ID; reuse with identical retry data.
    - `importRowId` string: Stable source row ID within an import.
    - `photo` union: Portrait upload or LinkedIn sourcing.
      - RecipientPhotoUpload
        - `source` 'upload', required: upload: owned asset; linkedin: staff sourcing.
        - `assetId` string, required: Owned portrait asset_ ID from upload/import.
        - `rightsConfirmed` true: True only with explicit photo-use permission.
      - RecipientPhotoLinkedIn
        - `source` 'linkedin', required: upload: owned asset; linkedin: staff sourcing.
        - `profileUrl` string, uri, required: Supplied personal profile URL; photo sourcing requires separate permission.
    - `adultRecipientConfirmed` boolean: Explicit 21+ and adult-signature confirmation.
    - `assetIds` string[]: Owned artwork asset IDs uploaded to this workspace.
    - `externalId` string: External record ID (recipient contact ID).
    - `email` union: An email address, or an empty string for none. Both variants are strings, so the union has no discriminator.
      - ''
      - string, email
    - `company` string: Recipient company; used for {{company}}.
    - `title` string: Recipient's job title.
    - `context` string: Recipient background from customer.
    - `customTag` string: Value for {{custom}} in this note.
    - `address` string: Street and unit; required when known.
    - `city` string: US city; required when known.
    - `state` string: US state/DC code; required when known.
    - `zip` string: US ZIP/ZIP+4; required when known.
    - `country` 'US': Delivery country; US only.
    - `addressMode` 'known' | 'confirm': known: complete sender-supplied US shipping address. confirm: address unknown; the recipient confirms it through a private gift link where the workspace allows it, including a qualified prepaid offer shown on the campaign preview. Otherwise live address-free gifts are refused with LIVE_ADDRESSLESS_UNAVAILABLE.
    - `message` string: Printed note; blank uses campaign fallback.
    - `details` object: Per-recipient gift details keyed by the gift's recipientDetails keys, such as companyLogo or team. Required ones must be filled before approval.

## Response `200`

Success: `{"data": PlaybookAddResult}`.

- PlaybookAddResultResponse: The success body: `{"data": PlaybookAddResult}`.
  - `data` PlaybookAddResult, required: What happened to one person added to a playbook. Read `approval`.
    - `queued` boolean, required: true while the person waits or is scheduled; false once a gift exists or they were skipped.
    - `recipientId` string, required
    - `alreadyQueued` boolean, required
    - `approvalUrl` string, required: Where a signed-in owner or admin approves waiting gifts for this playbook.
    - `approval` union, required: auto_approved: the auto-approve rule approved the gift and paid it from the balance (sendId set). approved: a person already approved it (sendId set). waiting: it waits for a person (reason set). scheduled: the auto-approve rule finishes it in the background. skipped: a person skipped this recipient.
      - PlaybookApprovalAutoApproved: The playbook's auto-approve rule approved the gift and paid it from the balance.
        - `state` 'auto_approved', required
        - `sendId` string, required: The gift (send_).
      - PlaybookApprovalApproved: A person already approved the gift.
        - `state` 'approved', required
        - `sendId` string, required: The gift (send_).
      - PlaybookApprovalWaiting: The person waits for a person to approve; `reason` says why. Nothing is charged or sent.
        - `state` 'waiting', required
        - `reason` 'auto_approve_off' | 'over_gift_limit' | 'monthly_cap' | 'low_balance' | 'repeat_recipient' | 'photo_review' | 'age_restricted' | 'shipping_quote' | 'email_unverified' | 'blocked', required: Why the person waits for a person: auto_approve_off (no auto-approve on this playbook), over_gift_limit (the gift costs more than the per-gift limit), monthly_cap (the monthly budget is used up), low_balance (the balance cannot cover the gift; adding funds does not re-check waiting people), repeat_recipient (already gifted by another campaign), photo_review (a photo must be sourced or approved), age_restricted (alcohol always waits), shipping_quote (delivery is not priced yet, so these gifts always wait), email_unverified (the person who turned it on has not verified their email), blocked (anything else, such as an unavailable or unverified gift, or with mode live a person without a sender-supplied address).
      - PlaybookApprovalScheduled: The auto-approve rule finishes this person in the background; watch `GET /events` for `send.auto_approved`.
        - `state` 'scheduled', required
      - PlaybookApprovalSkipped: A person skipped this recipient; do not add them again.
        - `state` 'skipped', required

## Other responses

- `400`: Bad request: `VALIDATION_ERROR`, `INVALID_JSON`. Each example says when.
- `401`: Not authenticated: `AUTH_REQUIRED`. no credentials, or an OAuth token on a route that only takes workspace keys; the key is malformed, unknown or revoked.
- `403`: Forbidden: `ARTWORK_UNAVAILABLE`, `RECIPIENT_SCOPE`. Each example says when.
- `404`: Not found: `NOT_FOUND`. Each example says when.
- `409`: Conflict: `NOT_ALWAYS_ON`, `PLAYBOOK_OFF`, `CHECKOUT_LOCKED`, `RECIPIENT_LIMIT`, `PLAYBOOK_BUSY`, `GIFT_RETIRED`, `GIFT_UNAVAILABLE`, `PRICE_QUOTE_REQUIRED`, `RECIPIENT_IDENTITY_CONFLICT`, `DUPLICATE_RECIPIENT`. Each example says when.
- `413`: Request too large: `INVALID_REQUEST`. the body exceeds 1 MiB.
- `415`: Unsupported media type: `INVALID_REQUEST`. a write with a body that is not `Content-Type: application/json` (a key request with no body needs no Content-Type).
- `429`: Too many requests: `RATE_LIMIT`. more than 240 requests in the current minute for this workspace (REST and MCP share the limit); wait `retryAfterMs`.
- `default`: Any other refusal or failure, such as 403 CSRF (a cross-origin `Origin` header), 403 HTTPS_REQUIRED or 500 INTERNAL_ERROR, with what to do next.

---

[API](https://skmtc.dev/gtmswag/apis/gtm-swag-api.md) · [All operations](https://skmtc.dev/gtmswag/apis/gtm-swag-api/llms.txt) · [OpenAPI document](https://skmtc.dev/gtmswag/apis/gtm-swag-api/revisions/9ff86fa99afe?raw)
