Proposal API by Unfairly
OpenAPIAPI & webhooks

Proposal API

Send us a request and we build a proposal for your team to review and publish. Then we tell your systems, signed, every time it changes or gets read.

Requests in

Your CRM or portal posts the deal's facts. We build the proposal.

Events out

We post to your endpoint as each proposal changes and gets read.

Target contract — backend adoption is in progress. This reference does not establish deployed support. Existing request/response behavior may differ until qualification.

Base URL https://app.unfairly.ai/api/decks/proposals. The whole API is also an OpenAPI 3.1 document: import it into Postman or Insomnia, generate a client, or hand it to your AI coding tool.

Quickstart

  1. Get an intake key

    An owner or admin opens API & webhooks and chooses New key. It shows once.

  2. Send a dry run

    Save the request beside it as request.json and send it with ?dry_run=1. Its inputs are empty; acceptance depends on your kit and inherited role assignments. Fill them in from your schema until it answers 200.

  3. Create and listen

    Drop dry_run, send a unique Idempotency-Key, set your endpoint on the same page, and verify each event's signature.

request.jsonJSON
{
  "request_id": "portal:deal-123:proposal",
  "callback_url": "https://example.com/webhooks/proposals",
  "source": "portal",
  "recipient": {
    "name": "Acme Logistics"
  },
  "owner": {
    "name": "Dana Ruiz",
    "email": "dana@example.com"
  },
  "initial_roles": [
    {
      "email": "dana@example.com",
      "role": "editor"
    }
  ],
  "proposal": {
    "inputs": {}
  }
}
Dry run
curl -X POST "https://app.unfairly.ai/api/decks/proposals/requests?dry_run=1" \
  -H "Authorization: Bearer $INTAKE_KEY" \
  -H "Content-Type: application/json" \
  -d @request.json

Try it

Paste your intake key to call the API from this page: read your workspace's schema, dry-run a request against it, or read a job. The key stays in this tab and goes only to https://app.unfairly.ai/api/decks/proposals. Nothing here builds a proposal.

After Get my schema, the request's inputs list your kit's fields.
Response
Nothing sent yet.

Authentication

Every call carries your intake key: Authorization: Bearer <intake key>. Events we send you are signed with a separate signing secret (unfcb_…).

An owner or admin of your workspace gets both on the API & webhooks page: New key makes an intake key and shows it once (the old one keeps working for 72 hours), and Reveal shows the signing secret. Keep them in your secrets manager, never in code or chat.

Send only de-identified facts. Send aggregate employer facts, not individual employee/claims records. Authorized business contacts in owner and initial_roles are distinct from prohibited employee data.

HeaderHTTP
Authorization: Bearer <intake key>

Errors

A refusal contains error_code and error; optional details entries carry a JSON Pointer path, code and message. Each endpoint lists the codes it can answer.

  • 200Read result, dry-run result, or completed operation acknowledgment.
  • 202Durably accepted operation; follow status_url for its result.
  • 400invalid_input, invalid_page_token or page_token_expired: correct the request. Validation before acceptance does not consume a new key.
  • 401unauthorized: provision a valid workspace key.
  • 403forbidden: unavailable to this credential/workspace.
  • 404not_found: unknown, foreign-workspace or expired resource.
  • 409idempotency_conflict, idempotency_in_progress, invalid_state, or (DELETE only) signed_agreement. An in-progress reservation includes Retry-After: 1.
  • 410proposal_deleted: stop proposal actions; retained operation receipts remain readable.
  • 413payload_too_large: maximum 262144 UTF-8 bytes.
  • 415unsupported_media_type: send application/json.
  • 422invalid_input or prohibited_data: correct semantic inputs; no work accepted.
  • 429rate_limited: retry unchanged after Retry-After.
  • 500internal_error: outcome may be uncertain; retry unchanged with the same key or look it up.
  • 503temporarily_unavailable: retry unchanged after Retry-After; backlog admission uses 60 seconds.
422JSON
{
  "error_code": "invalid_input",
  "error": "The inputs do not match the kit schema.",
  "details": [
    {
      "path": "/inputs/census/eligible",
      "code": "required",
      "message": "Eligible count is required."
    }
  ]
}

Rate limits

Workspace limits: build-triggering writes 60 a minute, other writes 60 a minute, reads 600 a minute. Keys and server instances share these budgets. At most 1,000 unfinished build operations are admitted per workspace; excess returns 503 and Retry-After: 60. Rate rejection is 429 with Retry-After.

A request body can be up to 256 KB; a larger one answers 413 unread.

429HTTP
HTTP/1.1 429 Too Many Requests
Retry-After: 23

{"error_code":"rate_limited","error":"Workspace rate limit exceeded. Retry in 23 seconds."}

Send a request

POST/requests

Validate all input and initial roles before grants. A fresh key with an existing active request_id is a completed no-op, not an update. A failed first build with no successful version may restart with corrected inputs/current kit under a fresh key, preserving job_id. Same-key retries replay the original acknowledgment before changed-kit validation. ?dry_run=1 or ?dry_run=true validates with no side effects and needs no Idempotency-Key.

Query

FieldType and notes
dry_runenum

1 or true validates only: no operation, grant, email or build. A 200 is DryRun, never Ack. Without this parameter, 200 is Ack.

one of 1, true

Body

FieldType and notes
request_idrequiredstring

Your id for this proposal. Within the workspace it identifies one job; use Idempotency-Key to retry an action.

letters, numbers and . _ : -, starting with a letter or number, up to 100 characters

sourcerequiredenum

Where the request came from: crm, portal, inbox, chat, api.

one of crm, portal, inbox, chat, api

recipientobject

Optional { name }: who the proposal is for. Omit when the employer is unknown.

recipient.namerequiredstring

Who the proposal is for, as it should read on the page.

at least 1 characters · up to 200 characters

ownerrequiredobject

{ name, email, booking_url? }: your person on the deal. Role assignments are separate.

owner.namerequiredstring

Your person on the deal.

at least 1 characters · up to 120 characters

owner.emailrequiredstring (email)

Your person's email. Does not assign an Editor or Commenter role.

matches ^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$

owner.booking_urlstring (url)

Fills the book_meeting button when ctas doesn't set one and this is a valid HTTPS link.

viewersarray of string (email)

Omit or send []. Initial Sharer/Viewer grants are not accepted.

allowed_viewersobject

Verified viewers of the approved proposal. API updates add emails and exact domains; omitted entries and empty arrays never remove access. Manage explicit removals in Share. No invitations or staff roles are granted.

allowed_viewers.emailsarray of string (email)

up to 1000 items

allowed_viewers.domainsarray of string

up to 1000 items

ctasobject

{ book_meeting?, accept? }, each { url, label? }, https only. Shown as buttons; clicks come back as proposal.cta_clicked.

ctas.book_meetingobject

A button that books a meeting: { url, label? }, https only.

ctas.acceptobject

A button that accepts the proposal: { url, label? }, https only.

notesstring

Optional staff-written context or instructions, up to 8,000 characters. Notes are not content-screened; the sender controls what is included.

up to 8,000 characters

signersarray of object

Up to 10 { role, name, email, title?, order? }. When a version is promoted it goes out for signature: each signer confirms their email with a code, fills their fields (the kit's "x-signer" fields for their role) and signs on the page.

up to 10 items

signatureobject

{ expires_in_days?, cc? }: days until the signature request expires (30 by default), and up to 10 emails that get the signed copy and nothing else.

signature.expires_in_daysinteger
signature.ccarray of string (email)

up to 10 items

callback_urlrequiredstring (url)

Required HTTPS event endpoint for this proposal. Stored at creation and reused for later events; no workspace default is needed.

matches ^https:\/\/

proposalrequiredobject
proposal.templatestring, or null

One of the kit’s template ids, or null to let the kit choose.

at least 1 characters · up to 80 characters

proposal.inputsrequiredobject

Customer-specific facts validated by the workspace kit. No employee records or identifying health data.

initial_rolesarray of object

Workspace-wide add/upgrade assignments. Omission never revokes access. Normalize email; conflicting roles for one email are invalid. Require an inherited or assigned Editor. Invitations are sent for new/upgraded access.

up to 50 items

The full request is in Request fields. Your kit's inputs come from GET /schema.

Responses

  • 200Ack for completed creation/no-op; DryRun only when dry_run=1 or dry_run=true.
  • 202Durably accepted; work remains. Poll status_url.
  • 400invalid_input, invalid_page_token or page_token_expired: correct the request. Validation before acceptance does not consume a new key.
  • 401unauthorized: provision a valid workspace key.
  • 403forbidden: unavailable to this credential/workspace.
  • 404not_found: unknown, foreign-workspace or expired resource.
  • 409idempotency_conflict, idempotency_in_progress, invalid_state, or (DELETE only) signed_agreement. An in-progress reservation includes Retry-After: 1.
  • 410proposal_deleted: stop proposal actions; retained operation receipts remain readable.
  • 413payload_too_large: maximum 262144 UTF-8 bytes.
  • 415unsupported_media_type: send application/json.
  • 422invalid_input or prohibited_data: correct semantic inputs; no work accepted.
  • 429rate_limited: retry unchanged after Retry-After.
  • 500internal_error: outcome may be uncertain; retry unchanged with the same key or look it up.
  • 503temporarily_unavailable: retry unchanged after Retry-After; backlog admission uses 60 seconds.

Counts toward the rate limit.

request.jsonJSON
{
  "request_id": "portal:deal-123:proposal",
  "callback_url": "https://example.com/webhooks/proposals",
  "source": "portal",
  "recipient": {
    "name": "Acme Logistics"
  },
  "owner": {
    "name": "Dana Ruiz",
    "email": "dana@example.com"
  },
  "initial_roles": [
    {
      "email": "dana@example.com",
      "role": "editor"
    }
  ],
  "proposal": {
    "inputs": {}
  }
}
POST /requests
curl -X POST "https://app.unfairly.ai/api/decks/proposals/requests?dry_run=1" \
  -H "Authorization: Bearer $INTAKE_KEY" \
  -H "Content-Type: application/json" \
  -d @request.json
Response · 200JSON
{
  "operation_id": "7f3e9c2a-0000-4000-8000-000000000001",
  "job_id": "4db5a295-7c1e-4f8a-9d2b-3e6f0a1b2c3d",
  "request_id": "portal:deal-123:proposal",
  "status": "succeeded",
  "status_url": "https://app.unfairly.ai/api/decks/proposals/operations/7f3e9c2a-0000-4000-8000-000000000001"
}

Get current contract manifest

GET/contract

Fetch the current combined OpenAPI at the stable URL GET /api/decks/proposals/contract/openapi.json using Authorization: Bearer <intake_key>. GET /api/decks/proposals/contract returns contract_id and permanent artifact URLs; pin a version with GET /api/decks/proposals/contracts/{contract_id}/openapi.json. The current URL follows contract updates; the permanent URL retains the exact version. GET /api/decks/proposals/schema remains available separately. Fresh enabled execution-context authorization is required on every read, including historical artifacts. IDs bind API version, resolved kit commit, context and exact artifact bytes. Historical artifacts are retained immutable bytes; never regenerate them from current code.

Responses

  • 200Get current contract manifest
  • 401unauthorized: provision a valid workspace key.
  • 404not_found: unknown, foreign-workspace or expired resource.
  • 429rate_limited: retry unchanged after Retry-After.
  • 503temporarily_unavailable: retry unchanged after Retry-After; backlog admission uses 60 seconds.

Counts toward the rate limit.

GET /contract
curl "https://app.unfairly.ai/api/decks/proposals/contract" \
  -H "Authorization: Bearer $INTAKE_KEY"

Download current contract artifact

GET/contract/{artifact}

Fetch the current combined OpenAPI at the stable URL GET /api/decks/proposals/contract/openapi.json using Authorization: Bearer <intake_key>. GET /api/decks/proposals/contract returns contract_id and permanent artifact URLs; pin a version with GET /api/decks/proposals/contracts/{contract_id}/openapi.json. The current URL follows contract updates; the permanent URL retains the exact version. GET /api/decks/proposals/schema remains available separately. Fresh enabled execution-context authorization is required on every read, including historical artifacts. IDs bind API version, resolved kit commit, context and exact artifact bytes. Historical artifacts are retained immutable bytes; never regenerate them from current code.

Responses

  • 200Download current contract artifact
  • 401unauthorized: provision a valid workspace key.
  • 404not_found: unknown, foreign-workspace or expired resource.
  • 429rate_limited: retry unchanged after Retry-After.
  • 503temporarily_unavailable: retry unchanged after Retry-After; backlog admission uses 60 seconds.

Counts toward the rate limit.

GET /contract/{artifact}
curl "https://app.unfairly.ai/api/decks/proposals/contract/{artifact}" \
  -H "Authorization: Bearer $INTAKE_KEY"

Download immutable contract artifact

GET/contracts/{contractId}/{artifact}

Fetch the current combined OpenAPI at the stable URL GET /api/decks/proposals/contract/openapi.json using Authorization: Bearer <intake_key>. GET /api/decks/proposals/contract returns contract_id and permanent artifact URLs; pin a version with GET /api/decks/proposals/contracts/{contract_id}/openapi.json. The current URL follows contract updates; the permanent URL retains the exact version. GET /api/decks/proposals/schema remains available separately. Fresh enabled execution-context authorization is required on every read, including historical artifacts. IDs bind API version, resolved kit commit, context and exact artifact bytes. Historical artifacts are retained immutable bytes; never regenerate them from current code.

Responses

  • 200Download immutable contract artifact
  • 401unauthorized: provision a valid workspace key.
  • 404not_found: unknown, foreign-workspace or expired resource.
  • 429rate_limited: retry unchanged after Retry-After.
  • 503temporarily_unavailable: retry unchanged after Retry-After; backlog admission uses 60 seconds.

Counts toward the rate limit.

GET /contracts/{contractId}/{artifact}
curl "https://app.unfairly.ai/api/decks/proposals/contracts/{contractId}/{artifact}" \
  -H "Authorization: Bearer $INTAKE_KEY"

Read safe request diagnostics

GET/diagnostics

One logical diagnostic per authenticated attempt identity, including malformed requests without a job. Replay preserves its ID; later acceptance upgrades a refusal, never the reverse. No payload, notes, raw errors, credentials or caller request IDs are retained in these public rows. Fresh context authorization, signed context-bound pagination and a 30-day read window apply. Unauthenticated correlation IDs are not customer receipts.

Query

FieldType and notes
diagnostic_idstring (uuid)

Optional safe diagnostic ID; lookup remains scoped to this authenticated context and retention window.

page_sizeinteger

Results per page, default 50.

page_tokenstring

Opaque signed continuation; expires after 15 minutes and is bound to this context.

Responses

  • 200Read safe request diagnostics
  • 400invalid_input, invalid_page_token or page_token_expired: correct the request. Validation before acceptance does not consume a new key.
  • 401unauthorized: provision a valid workspace key.
  • 403forbidden: unavailable to this credential/workspace.
  • 404not_found: unknown, foreign-workspace or expired resource.
  • 410proposal_deleted: stop proposal actions; retained operation receipts remain readable.
  • 429rate_limited: retry unchanged after Retry-After.
  • 500internal_error: outcome may be uncertain; retry unchanged with the same key or look it up.
  • 503temporarily_unavailable: retry unchanged after Retry-After; backlog admission uses 60 seconds.

Counts toward the rate limit.

GET /diagnostics
curl "https://app.unfairly.ai/api/decks/proposals/diagnostics" \
  -H "Authorization: Bearer $INTAKE_KEY"

Download the exact approved PDF

GET/jobs/{job_id}/versions/{version}/pdf

Authenticate with the intake key in the Authorization header, without cookies. Only the current approved version is downloadable. A newer draft leaves these bytes unchanged; replacement revokes the previous version. Withdrawal retains retrieval; deletion acceptance immediately revokes it. Returns private stored bytes after hash/size verification and fresh authorization before and after storage access; no signed redirect or credential-bearing URL.

Responses

  • 200Download the exact approved PDF
  • 400invalid_input, invalid_page_token or page_token_expired: correct the request. Validation before acceptance does not consume a new key.
  • 401unauthorized: provision a valid workspace key.
  • 403forbidden: unavailable to this credential/workspace.
  • 404not_found: unknown, foreign-workspace or expired resource.
  • 409idempotency_conflict, idempotency_in_progress, invalid_state, or (DELETE only) signed_agreement. An in-progress reservation includes Retry-After: 1.
  • 410proposal_deleted: stop proposal actions; retained operation receipts remain readable.
  • 429rate_limited: retry unchanged after Retry-After.
  • 500internal_error: outcome may be uncertain; retry unchanged with the same key or look it up.
  • 503temporarily_unavailable: retry unchanged after Retry-After; backlog admission uses 60 seconds.

Counts toward the rate limit.

GET /jobs/{job_id}/versions/{version}/pdf
curl "https://app.unfairly.ai/api/decks/proposals/jobs/$JOB_ID/versions/{version}/pdf" \
  -H "Authorization: Bearer $INTAKE_KEY"

Fetch exact successful draft inputs

GET/jobs/{job_id}/versions/{version}/inputs

Read the explicitly requested successful version before publication or after newer drafts exist. Inputs and contract identity are immutable; approval is current at read time. Never substitutes latest working inputs or approved_inputs. Polling works without callback delivery. Missing, failed or unrecorded versions return 404. Withdrawal retains retrieval; deletion acceptance revokes it. Fresh intake-key/workspace/context authority applies; responses are private, no-store. Editors and Commenters request pricing changes through ordinary comments; an Editor approves publication of the resulting draft. No separate pricing authorization is required.

Responses

  • 200Fetch exact successful draft inputs
  • 400invalid_input, invalid_page_token or page_token_expired: correct the request. Validation before acceptance does not consume a new key.
  • 401unauthorized: provision a valid workspace key.
  • 403forbidden: unavailable to this credential/workspace.
  • 404not_found: unknown, foreign-workspace or expired resource.
  • 410proposal_deleted: stop proposal actions; retained operation receipts remain readable.
  • 429rate_limited: retry unchanged after Retry-After.
  • 500internal_error: outcome may be uncertain; retry unchanged with the same key or look it up.
  • 503temporarily_unavailable: retry unchanged after Retry-After; backlog admission uses 60 seconds.

Counts toward the rate limit.

GET /jobs/{job_id}/versions/{version}/inputs
curl "https://app.unfairly.ai/api/decks/proposals/jobs/$JOB_ID/versions/{version}/inputs" \
  -H "Authorization: Bearer $INTAKE_KEY"

Get your schema

GET/schema

Exact schema.json bytes from the current authenticated contract bundle; use its manifest and immutable URL to pin a contract.

Responses

  • 200Get your schema
  • 400invalid_input, invalid_page_token or page_token_expired: correct the request. Validation before acceptance does not consume a new key.
  • 401unauthorized: provision a valid workspace key.
  • 403forbidden: unavailable to this credential/workspace.
  • 404not_found: unknown, foreign-workspace or expired resource.
  • 410proposal_deleted: stop proposal actions; retained operation receipts remain readable.
  • 429rate_limited: retry unchanged after Retry-After.
  • 500internal_error: outcome may be uncertain; retry unchanged with the same key or look it up.
  • 503temporarily_unavailable: retry unchanged after Retry-After; backlog admission uses 60 seconds.

Counts toward the rate limit.

GET /schema
curl "https://app.unfairly.ai/api/decks/proposals/schema" \
  -H "Authorization: Bearer $INTAKE_KEY"

Get a job

GET/jobs/{job_id}

Without collection, return Job. With collection, return its Page. Analytics failure returns core fields and known PDF URLs with analytics_status=temporarily_unavailable. Published URL allocation is not publication. Review URL follows the latest successful draft.

Query

FieldType and notes
collectionenum

Omit for the full object. Otherwise return only the named Page. Invalid selector combinations are invalid_input.

one of rounds, publishes, versions, live_viewers, viewer_cta_clicks, anonymous_cta_clicks, engagement_versions, sections

page_sizeinteger

Default 50, maximum 100.

page_tokenstring

Opaque continuation token from next_url. Follow next_url verbatim; tokens expire after 24 hours and are bound to workspace/resource/collection/selectors.

viewer_emailstring (email)

Required only for viewer_cta_clicks; normalized email.

rendered_versioninteger

Required for sections; optional version scope for live_viewers, viewer_cta_clicks and anonymous_cta_clicks. Invalid for other collections.

Responses

  • 200Get a job
  • 400invalid_input, invalid_page_token or page_token_expired: correct the request. Validation before acceptance does not consume a new key.
  • 401unauthorized: provision a valid workspace key.
  • 403forbidden: unavailable to this credential/workspace.
  • 404not_found: unknown, foreign-workspace or expired resource.
  • 410proposal_deleted: stop proposal actions; retained operation receipts remain readable.
  • 429rate_limited: retry unchanged after Retry-After.
  • 500internal_error: outcome may be uncertain; retry unchanged with the same key or look it up.
  • 503temporarily_unavailable: retry unchanged after Retry-After; backlog admission uses 60 seconds.

Counts toward the rate limit.

GET /jobs/{job_id}
curl "https://app.unfairly.ai/api/decks/proposals/jobs/$JOB_ID" \
  -H "Authorization: Bearer $INTAKE_KEY"
Response · 200JSON
{
  "job_id": "4db5a295-7c1e-4f8a-9d2b-3e6f0a1b2c3d",
  "request_id": "portal:deal-123:proposal",
  "status": "in_review",
  "url": "https://proposals.example.com/acme",
  "live_url": "https://proposals.example.com/acme",
  "review_url": "https://review.pitchunfairly.com/acme",
  "version": 2,
  "live_version": 1,
  "promoted_at": "2026-10-19T14:00:00Z",
  "shape": null,
  "open_items": [],
  "revisions": 1,
  "requested_by": null,
  "promoted_by": null,
  "publishes": {
    "items": [
      {
        "version": 1,
        "at": "2026-10-19T14:00:00Z",
        "by": null
      }
    ],
    "next_url": null
  },
  "withdrawn_at": null,
  "withdrawn_by": null,
  "rounds": {
    "items": [
      {
        "round": 1,
        "kind": "update",
        "at": "2026-10-19T14:00:00Z",
        "by": null,
        "comments": 1
      }
    ],
    "next_url": null
  },
  "brief": null,
  "error": null,
  "updated_at": "2026-10-19T14:00:00Z",
  "pdf_url": null,
  "review_pdf_url": null,
  "engagement": null,
  "needs_attention": null,
  "analytics_status": "not_available",
  "versions": {
    "items": [
      {
        "version": 1,
        "created_at": "2026-10-19T14:00:00Z",
        "review_url": "https://review.pitchunfairly.com/acme?review&v=1",
        "review_pdf_url": null
      },
      {
        "version": 2,
        "created_at": "2026-10-19T14:00:00Z",
        "review_url": "https://review.pitchunfairly.com/acme?review&v=2",
        "review_pdf_url": null
      }
    ],
    "next_url": null
  },
  "deletion": null
}

Update a job

POST/jobs/{job_id}/update

Accept while busy and validate the merged accepted working state. Preserve custom partial merge: inputs deep-merge, arrays replace, omitted fields remain, notes append; not JSON Merge Patch. Latest accepted API/comment change wins. An unchanged update or {} is a no-op only if a successful draft represents that state; after a failed round it rebuilds retained context without repeating notes. Failed initial builds require creation restart.

Body

FieldType and notes
allowed_viewersobject

Verified viewers of the approved proposal. API updates add emails and exact domains; omitted entries and empty arrays never remove access. Manage explicit removals in Share. No invitations or staff roles are granted.

allowed_viewers.emailsarray of string (email)

up to 1000 items

allowed_viewers.domainsarray of string

up to 1000 items

sourceenum

Where the request came from: crm, portal, inbox, chat, api.

one of crm, portal, inbox, chat, api

recipientobject

Optional { name }: who the proposal is for. Omit when the employer is unknown.

recipient.namerequiredstring

Who the proposal is for, as it should read on the page.

at least 1 characters · up to 200 characters

ownerobject

Send only the owner fields that changed. Omitted fields remain unchanged.

owner.namestring

Your person on the deal.

at least 1 characters · up to 120 characters

owner.emailstring (email)

Your person's email. Does not assign an Editor or Commenter role.

matches ^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$

owner.booking_urlstring (url)

Fills the book_meeting button when ctas doesn't set one and this is a valid HTTPS link.

ctasobject

{ book_meeting?, accept? }, each { url, label? }, https only. Shown as buttons; clicks come back as proposal.cta_clicked.

ctas.book_meetingobject

A button that books a meeting: { url, label? }, https only.

ctas.acceptobject

A button that accepts the proposal: { url, label? }, https only.

notesstring

Append call notes, up to 8,000 characters. Omission keeps existing notes; this does not replace them.

up to 8,000 characters

removearray of string

Inputs to take out, by path in the kit's shape ("dates.confirm_by"), when a fact no longer applies. The kit must allow the field to be left out; a path the proposal doesn't have is ignored.

up to 20 items

commentstring

Why the update was sent, in plain words. Logged with it and given to the round that rebuilds.

at least 1 characters · up to 2,000 characters

proposalobject

Omitted fields remain unchanged. Inputs merge recursively; arrays replace, null remains a value. A null template delegates selection to the kit.

proposal.templatestring, or null

One of the kit’s template ids, or null to let the kit choose.

at least 1 characters · up to 80 characters

proposal.inputsobject

Customer-specific facts validated by the workspace kit. No employee records or identifying health data.

Responses

  • 200Action already completed, including no-ops. Read status_url for the result.
  • 202Durably accepted; work remains. Poll status_url.
  • 400invalid_input, invalid_page_token or page_token_expired: correct the request. Validation before acceptance does not consume a new key.
  • 401unauthorized: provision a valid workspace key.
  • 403forbidden: unavailable to this credential/workspace.
  • 404not_found: unknown, foreign-workspace or expired resource.
  • 409idempotency_conflict, idempotency_in_progress, invalid_state, or (DELETE only) signed_agreement. An in-progress reservation includes Retry-After: 1.
  • 410proposal_deleted: stop proposal actions; retained operation receipts remain readable.
  • 413payload_too_large: maximum 262144 UTF-8 bytes.
  • 415unsupported_media_type: send application/json.
  • 422invalid_input or prohibited_data: correct semantic inputs; no work accepted.
  • 429rate_limited: retry unchanged after Retry-After.
  • 500internal_error: outcome may be uncertain; retry unchanged with the same key or look it up.
  • 503temporarily_unavailable: retry unchanged after Retry-After; backlog admission uses 60 seconds.

Counts toward the rate limit.

POST /jobs/{job_id}/update
curl -X POST "https://app.unfairly.ai/api/decks/proposals/jobs/$JOB_ID/update" \
  -H "Authorization: Bearer $INTAKE_KEY" -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"notes":"Use the 2027 plan year."}'
Response · 200JSON
{
  "operation_id": "7f3e9c2a-0000-4000-8000-000000000001",
  "job_id": "4db5a295-7c1e-4f8a-9d2b-3e6f0a1b2c3d",
  "request_id": "portal:deal-123:proposal",
  "status": "succeeded",
  "status_url": "https://app.unfairly.ai/api/decks/proposals/operations/7f3e9c2a-0000-4000-8000-000000000001"
}

Revise a job

POST/jobs/{job_id}/revise

Exactly one of comments or from_review. Accept while busy. Snapshot eligible unapplied comments at acceptance; no eligible comments is a successful no-op. Failure releases reservations but retains original acceptance order; a successful draft marks applied, not human-resolved. Direct comments append; failed direct-comment work can be recovered with an empty update.

Body

FieldType and notes
commentsrequiredarray of string

One to twenty comments, each up to 2,000 characters.

at least 1 · up to 20 items

or

FieldType and notes
from_reviewrequiredtrue

Apply eligible, unapplied review comments selected when this request is accepted.

Responses

  • 200Action already completed, including no-ops. Read status_url for the result.
  • 202Durably accepted; work remains. Poll status_url.
  • 400invalid_input, invalid_page_token or page_token_expired: correct the request. Validation before acceptance does not consume a new key.
  • 401unauthorized: provision a valid workspace key.
  • 403forbidden: unavailable to this credential/workspace.
  • 404not_found: unknown, foreign-workspace or expired resource.
  • 409idempotency_conflict, idempotency_in_progress, invalid_state, or (DELETE only) signed_agreement. An in-progress reservation includes Retry-After: 1.
  • 410proposal_deleted: stop proposal actions; retained operation receipts remain readable.
  • 413payload_too_large: maximum 262144 UTF-8 bytes.
  • 415unsupported_media_type: send application/json.
  • 422invalid_input or prohibited_data: correct semantic inputs; no work accepted.
  • 429rate_limited: retry unchanged after Retry-After.
  • 500internal_error: outcome may be uncertain; retry unchanged with the same key or look it up.
  • 503temporarily_unavailable: retry unchanged after Retry-After; backlog admission uses 60 seconds.

Counts toward the rate limit.

POST /jobs/{job_id}/revise
curl -X POST "https://app.unfairly.ai/api/decks/proposals/jobs/$JOB_ID/revise" \
  -H "Authorization: Bearer $INTAKE_KEY" -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"comments":["Lead with savings."]}'
Response · 200JSON
{
  "operation_id": "7f3e9c2a-0000-4000-8000-000000000001",
  "job_id": "4db5a295-7c1e-4f8a-9d2b-3e6f0a1b2c3d",
  "request_id": "portal:deal-123:proposal",
  "status": "succeeded",
  "status_url": "https://app.unfairly.ai/api/decks/proposals/operations/7f3e9c2a-0000-4000-8000-000000000001"
}

Promote a version

POST/jobs/{job_id}/promote

Version is required; missing or unavailable version returns 422. Publish any available successful version, including older ones with unresolved comments. Preserve newer working data, draft history and the live URL. Pending builds never auto-publish; no-op when already live. Withdrawn/deleted proposals cannot publish.

Body

FieldType and notes
versionrequiredinteger

The exact available version to publish, including an older version. No implicit newest-version selection.

positive

Responses

  • 200Action already completed, including no-ops. Read status_url for the result.
  • 202Durably accepted; work remains. Poll status_url.
  • 400invalid_input, invalid_page_token or page_token_expired: correct the request. Validation before acceptance does not consume a new key.
  • 401unauthorized: provision a valid workspace key.
  • 403forbidden: unavailable to this credential/workspace.
  • 404not_found: unknown, foreign-workspace or expired resource.
  • 409idempotency_conflict, idempotency_in_progress, invalid_state, or (DELETE only) signed_agreement. An in-progress reservation includes Retry-After: 1.
  • 410proposal_deleted: stop proposal actions; retained operation receipts remain readable.
  • 413payload_too_large: maximum 262144 UTF-8 bytes.
  • 415unsupported_media_type: send application/json.
  • 422invalid_input or prohibited_data: correct semantic inputs; no work accepted.
  • 429rate_limited: retry unchanged after Retry-After.
  • 500internal_error: outcome may be uncertain; retry unchanged with the same key or look it up.
  • 503temporarily_unavailable: retry unchanged after Retry-After; backlog admission uses 60 seconds.

Counts toward the rate limit.

POST /jobs/{job_id}/promote
curl -X POST "https://app.unfairly.ai/api/decks/proposals/jobs/$JOB_ID/promote" \
  -H "Authorization: Bearer $INTAKE_KEY" -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"version":1}'
Response · 200JSON
{
  "operation_id": "7f3e9c2a-0000-4000-8000-000000000001",
  "job_id": "4db5a295-7c1e-4f8a-9d2b-3e6f0a1b2c3d",
  "request_id": "portal:deal-123:proposal",
  "status": "succeeded",
  "status_url": "https://app.unfairly.ai/api/decks/proposals/operations/7f3e9c2a-0000-4000-8000-000000000001"
}

Withdraw a job

POST/jobs/{job_id}/withdraw

Disable draft/live/PDF access before success acknowledgment. Cancel pending work and prevent late restoration. Retain records; repeated withdrawal is successful no-op. Body is an object; reason is optional.

Body

FieldType and notes
reasonstring

Why the proposal is being withdrawn.

at least 1 characters · up to 500 characters

Responses

  • 200Action already completed, including no-ops. Read status_url for the result.
  • 202Durably accepted; work remains. Poll status_url.
  • 400invalid_input, invalid_page_token or page_token_expired: correct the request. Validation before acceptance does not consume a new key.
  • 401unauthorized: provision a valid workspace key.
  • 403forbidden: unavailable to this credential/workspace.
  • 404not_found: unknown, foreign-workspace or expired resource.
  • 409idempotency_conflict, idempotency_in_progress, invalid_state, or (DELETE only) signed_agreement. An in-progress reservation includes Retry-After: 1.
  • 410proposal_deleted: stop proposal actions; retained operation receipts remain readable.
  • 413payload_too_large: maximum 262144 UTF-8 bytes.
  • 415unsupported_media_type: send application/json.
  • 422invalid_input or prohibited_data: correct semantic inputs; no work accepted.
  • 429rate_limited: retry unchanged after Retry-After.
  • 500internal_error: outcome may be uncertain; retry unchanged with the same key or look it up.
  • 503temporarily_unavailable: retry unchanged after Retry-After; backlog admission uses 60 seconds.

Counts toward the rate limit.

POST /jobs/{job_id}/withdraw
curl -X POST "https://app.unfairly.ai/api/decks/proposals/jobs/$JOB_ID/withdraw" \
  -H "Authorization: Bearer $INTAKE_KEY" -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
Response · 200JSON
{
  "operation_id": "7f3e9c2a-0000-4000-8000-000000000001",
  "job_id": "4db5a295-7c1e-4f8a-9d2b-3e6f0a1b2c3d",
  "request_id": "portal:deal-123:proposal",
  "status": "succeeded",
  "status_url": "https://app.unfairly.ai/api/decks/proposals/operations/7f3e9c2a-0000-4000-8000-000000000001"
}

List viewers

GET/jobs/{job_id}/viewers

Without collection return AccessView. Reviewer is named Commenter in this contract. Preserve pending requests plus decided requests from the last 30 days.

Query

FieldType and notes
collectionenum

Omit for the full object. Otherwise return only the named Page. Invalid selector combinations are invalid_input.

one of viewers, requests

page_sizeinteger

Default 50, maximum 100.

page_tokenstring

Opaque continuation token from next_url. Follow next_url verbatim; tokens expire after 24 hours and are bound to workspace/resource/collection/selectors.

Responses

  • 200List viewers
  • 400invalid_input, invalid_page_token or page_token_expired: correct the request. Validation before acceptance does not consume a new key.
  • 401unauthorized: provision a valid workspace key.
  • 403forbidden: unavailable to this credential/workspace.
  • 404not_found: unknown, foreign-workspace or expired resource.
  • 410proposal_deleted: stop proposal actions; retained operation receipts remain readable.
  • 429rate_limited: retry unchanged after Retry-After.
  • 500internal_error: outcome may be uncertain; retry unchanged with the same key or look it up.
  • 503temporarily_unavailable: retry unchanged after Retry-After; backlog admission uses 60 seconds.

Counts toward the rate limit.

GET /jobs/{job_id}/viewers
curl "https://app.unfairly.ai/api/decks/proposals/jobs/$JOB_ID/viewers" \
  -H "Authorization: Bearer $INTAKE_KEY"

Change viewers

POST/jobs/{job_id}/viewers

Direct add grants Viewer without emailing. Approving a request grants Viewer and reports its email attempt separately. Validate conflicts before side effects; runtime mixed results terminate failed with partial_failure and every item's result. Successful grants remain. No grants after withdrawal/deletion.

Body

FieldType and notes
addarray of string (email)

Emails to grant Viewer access to the live link; no invitation email.

up to 50 items

revokearray of string (email)

Emails who may no longer open the live link.

up to 50 items

revoke_addedboolean

With revoke: also remove everyone the revoked people added.

decidearray of object

Approve or decline requests for Viewer access; approve_domain also updates the domain policy.

up to 50 items

audience_domainsarray of string

Company domains used by the sharing policy. Domains do not grant roles.

up to 20 items

invite_policyenum

colleagues: authorized sharers can invite within audience domains. approval: additions require approval. off: sharing invitations are disabled. Viewers cannot invite.

one of colleagues, approval, off

Responses

  • 200Action already completed, including no-ops. Read status_url for the result.
  • 202Durably accepted; work remains. Poll status_url.
  • 400invalid_input, invalid_page_token or page_token_expired: correct the request. Validation before acceptance does not consume a new key.
  • 401unauthorized: provision a valid workspace key.
  • 403forbidden: unavailable to this credential/workspace.
  • 404not_found: unknown, foreign-workspace or expired resource.
  • 409idempotency_conflict, idempotency_in_progress, invalid_state, or (DELETE only) signed_agreement. An in-progress reservation includes Retry-After: 1.
  • 410proposal_deleted: stop proposal actions; retained operation receipts remain readable.
  • 413payload_too_large: maximum 262144 UTF-8 bytes.
  • 415unsupported_media_type: send application/json.
  • 422invalid_input or prohibited_data: correct semantic inputs; no work accepted.
  • 429rate_limited: retry unchanged after Retry-After.
  • 500internal_error: outcome may be uncertain; retry unchanged with the same key or look it up.
  • 503temporarily_unavailable: retry unchanged after Retry-After; backlog admission uses 60 seconds.

Counts toward the rate limit.

POST /jobs/{job_id}/viewers
curl -X POST "https://app.unfairly.ai/api/decks/proposals/jobs/$JOB_ID/viewers" \
  -H "Authorization: Bearer $INTAKE_KEY" -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"add":["viewer@example.com"]}'
Response · 200JSON
{
  "operation_id": "7f3e9c2a-0000-4000-8000-000000000001",
  "job_id": "4db5a295-7c1e-4f8a-9d2b-3e6f0a1b2c3d",
  "request_id": "portal:deal-123:proposal",
  "status": "succeeded",
  "status_url": "https://app.unfairly.ai/api/decks/proposals/operations/7f3e9c2a-0000-4000-8000-000000000001"
}

Read captured events

GET/callbacks/capture/{workspace}

Ancillary testing surface; not required for integration. Read authenticated captured callbacks. Capture verifies raw sha256 signatures and known event schemas before redacting stored attribution.

Responses

  • 200Read captured events
  • 400invalid_input, invalid_page_token or page_token_expired: correct the request. Validation before acceptance does not consume a new key.
  • 401unauthorized: provision a valid workspace key.
  • 403forbidden: unavailable to this credential/workspace.
  • 404not_found: unknown, foreign-workspace or expired resource.
  • 410proposal_deleted: stop proposal actions; retained operation receipts remain readable.
  • 429rate_limited: retry unchanged after Retry-After.
  • 500internal_error: outcome may be uncertain; retry unchanged with the same key or look it up.
  • 503temporarily_unavailable: retry unchanged after Retry-After; backlog admission uses 60 seconds.

Counts toward the rate limit.

GET /callbacks/capture/{workspace}
curl "https://app.unfairly.ai/api/decks/proposals/callbacks/capture/$WORKSPACE" \
  -H "Authorization: Bearer $INTAKE_KEY"

Look up proposal by request ID

GET/requests/{request_id}

Read-only lookup without current-kit validation. URL-encode the request_id segment. 410 while a deletion receipt is retained; otherwise 404.

Responses

  • 200Look up proposal by request ID
  • 400invalid_input, invalid_page_token or page_token_expired: correct the request. Validation before acceptance does not consume a new key.
  • 401unauthorized: provision a valid workspace key.
  • 403forbidden: unavailable to this credential/workspace.
  • 404not_found: unknown, foreign-workspace or expired resource.
  • 410proposal_deleted: stop proposal actions; retained operation receipts remain readable.
  • 429rate_limited: retry unchanged after Retry-After.
  • 500internal_error: outcome may be uncertain; retry unchanged with the same key or look it up.
  • 503temporarily_unavailable: retry unchanged after Retry-After; backlog admission uses 60 seconds.

Counts toward the rate limit.

GET /requests/{request_id}
curl "https://app.unfairly.ai/api/decks/proposals/requests/$REQUEST_ID" \
  -H "Authorization: Bearer $INTAKE_KEY"
Response · 200JSON
{
  "job_id": "4db5a295-7c1e-4f8a-9d2b-3e6f0a1b2c3d",
  "request_id": "portal:deal-123:proposal",
  "status": "in_review",
  "url": "https://proposals.example.com/acme",
  "live_url": "https://proposals.example.com/acme",
  "review_url": "https://review.pitchunfairly.com/acme",
  "version": 2,
  "live_version": 1,
  "promoted_at": "2026-10-19T14:00:00Z",
  "shape": null,
  "open_items": [],
  "revisions": 1,
  "requested_by": null,
  "promoted_by": null,
  "publishes": {
    "items": [
      {
        "version": 1,
        "at": "2026-10-19T14:00:00Z",
        "by": null
      }
    ],
    "next_url": null
  },
  "withdrawn_at": null,
  "withdrawn_by": null,
  "rounds": {
    "items": [
      {
        "round": 1,
        "kind": "update",
        "at": "2026-10-19T14:00:00Z",
        "by": null,
        "comments": 1
      }
    ],
    "next_url": null
  },
  "brief": null,
  "error": null,
  "updated_at": "2026-10-19T14:00:00Z",
  "pdf_url": null,
  "review_pdf_url": null,
  "engagement": null,
  "needs_attention": null,
  "analytics_status": "not_available",
  "versions": {
    "items": [
      {
        "version": 1,
        "created_at": "2026-10-19T14:00:00Z",
        "review_url": "https://review.pitchunfairly.com/acme?review&v=1",
        "review_pdf_url": null
      },
      {
        "version": 2,
        "created_at": "2026-10-19T14:00:00Z",
        "review_url": "https://review.pitchunfairly.com/acme?review&v=2",
        "review_pdf_url": null
      }
    ],
    "next_url": null
  },
  "deletion": null
}

Delete proposal

DELETE/jobs/{job_id}

No body. DELETE is machine confirmation. A proposal whose site has an agreement with any signature (partly or fully signed) is refused with 409 signed_agreement: the signing record is kept for at least 7 years, so withdraw it instead; an agreement out for signature with no signature yet is voided and the delete proceeds. Disable hosted access/new work before acceptance; cancel pending work. Operation runs until active-system deletion completes. Poll the accepted operation for completion and its category receipt; deletion erases callback destinations and payloads, so no deletion-completion callback is sent. Keep only minimal redacted receipts for the retention window; return 410 for retained deleted identities, then 404. Cleanup durations are operational goals, not guarantees.

Responses

  • 200Action already completed, including no-ops. Read status_url for the result.
  • 202Durably accepted; work remains. Poll status_url.
  • 400invalid_input, invalid_page_token or page_token_expired: correct the request. Validation before acceptance does not consume a new key.
  • 401unauthorized: provision a valid workspace key.
  • 403forbidden: unavailable to this credential/workspace.
  • 404not_found: unknown, foreign-workspace or expired resource.
  • 409idempotency_conflict, idempotency_in_progress, invalid_state, or (DELETE only) signed_agreement. An in-progress reservation includes Retry-After: 1.
  • 410proposal_deleted: stop proposal actions; retained operation receipts remain readable.
  • 413payload_too_large: maximum 262144 UTF-8 bytes.
  • 415unsupported_media_type: send application/json.
  • 422invalid_input or prohibited_data: correct semantic inputs; no work accepted.
  • 429rate_limited: retry unchanged after Retry-After.
  • 500internal_error: outcome may be uncertain; retry unchanged with the same key or look it up.
  • 503temporarily_unavailable: retry unchanged after Retry-After; backlog admission uses 60 seconds.

Counts toward the rate limit.

DELETE /jobs/{job_id}
curl -X DELETE "https://app.unfairly.ai/api/decks/proposals/jobs/$JOB_ID" \
  -H "Authorization: Bearer $INTAKE_KEY" -H "Idempotency-Key: $IDEMPOTENCY_KEY"
Response · 200JSON
{
  "operation_id": "7f3e9c2a-0000-4000-8000-000000000001",
  "job_id": "4db5a295-7c1e-4f8a-9d2b-3e6f0a1b2c3d",
  "request_id": "portal:deal-123:proposal",
  "status": "succeeded",
  "status_url": "https://app.unfairly.ai/api/decks/proposals/operations/7f3e9c2a-0000-4000-8000-000000000001"
}

Read operation outcome

GET/operations/{operation_id}

Durable result for one action; no list endpoint. Unknown/foreign/expired receipts return 404. Queued/running result is null; terminal result contains outcome or is null with result_redacted=true after deletion. Invitation resend has null job_id/request_id.

Responses

  • 200Read operation outcome
  • 400invalid_input, invalid_page_token or page_token_expired: correct the request. Validation before acceptance does not consume a new key.
  • 401unauthorized: provision a valid workspace key.
  • 403forbidden: unavailable to this credential/workspace.
  • 404not_found: unknown, foreign-workspace or expired resource.
  • 410proposal_deleted: stop proposal actions; retained operation receipts remain readable.
  • 429rate_limited: retry unchanged after Retry-After.
  • 500internal_error: outcome may be uncertain; retry unchanged with the same key or look it up.
  • 503temporarily_unavailable: retry unchanged after Retry-After; backlog admission uses 60 seconds.

Counts toward the rate limit.

GET /operations/{operation_id}
curl "https://app.unfairly.ai/api/decks/proposals/operations/$OPERATION_ID" \
  -H "Authorization: Bearer $INTAKE_KEY"

Resend workspace invitation

POST/invitations/{invitation_id}/resend

Existing active workspace invitation only; no role change or rebuild. Unknown/foreign invitation: 404; consumed/revoked: 409. Return the current invitation ID in result.invitation, including a replacement ID when credentials rotate. Provider acceptance means sent, not delivered.

Body

FieldType and notes

Responses

  • 200Action already completed, including no-ops. Read status_url for the result.
  • 202Durably accepted; work remains. Poll status_url.
  • 400invalid_input, invalid_page_token or page_token_expired: correct the request. Validation before acceptance does not consume a new key.
  • 401unauthorized: provision a valid workspace key.
  • 403forbidden: unavailable to this credential/workspace.
  • 404not_found: unknown, foreign-workspace or expired resource.
  • 409idempotency_conflict, idempotency_in_progress, invalid_state, or (DELETE only) signed_agreement. An in-progress reservation includes Retry-After: 1.
  • 410proposal_deleted: stop proposal actions; retained operation receipts remain readable.
  • 413payload_too_large: maximum 262144 UTF-8 bytes.
  • 415unsupported_media_type: send application/json.
  • 422invalid_input or prohibited_data: correct semantic inputs; no work accepted.
  • 429rate_limited: retry unchanged after Retry-After.
  • 500internal_error: outcome may be uncertain; retry unchanged with the same key or look it up.
  • 503temporarily_unavailable: retry unchanged after Retry-After; backlog admission uses 60 seconds.

Counts toward the rate limit.

POST /invitations/{invitation_id}/resend
curl -X POST "https://app.unfairly.ai/api/decks/proposals/invitations/$INVITATION_ID/resend" \
  -H "Authorization: Bearer $INTAKE_KEY" -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
Response · 200JSON
{
  "operation_id": "7f3e9c2a-0000-4000-8000-000000000001",
  "job_id": "4db5a295-7c1e-4f8a-9d2b-3e6f0a1b2c3d",
  "request_id": "portal:deal-123:proposal",
  "status": "succeeded",
  "status_url": "https://app.unfairly.ai/api/decks/proposals/operations/7f3e9c2a-0000-4000-8000-000000000001"
}

The request

One JSON object per proposal. The recipient may be omitted when the employer is unknown. Optional allowed_viewers grants verified viewing access only; it never assigns workspace roles or sends invitations. API updates add supplied emails and domains to the current audience. Omission and empty arrays never remove access. Explicit removals are managed in Share, alongside existing people and roles. The access receipt reports the committed audience independently of content build status; content failure does not undo that receipt. Before approval no audience update exposes a draft. Unknown keys are refused. inputs is your kit's: GET /schema has every field, with its limits.

FieldType and notes
request_idrequiredstring

Your id for this proposal. Within the workspace it identifies one job; use Idempotency-Key to retry an action.

letters, numbers and . _ : -, starting with a letter or number, up to 100 characters

sourcerequiredenum

Where the request came from: crm, portal, inbox, chat, api.

one of crm, portal, inbox, chat, api

recipientobject

Optional { name }: who the proposal is for. Omit when the employer is unknown.

recipient.namerequiredstring

Who the proposal is for, as it should read on the page.

at least 1 characters · up to 200 characters

ownerrequiredobject

{ name, email, booking_url? }: your person on the deal. Role assignments are separate.

owner.namerequiredstring

Your person on the deal.

at least 1 characters · up to 120 characters

owner.emailrequiredstring (email)

Your person's email. Does not assign an Editor or Commenter role.

matches ^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$

owner.booking_urlstring (url)

Fills the book_meeting button when ctas doesn't set one and this is a valid HTTPS link.

viewersarray of string (email)

Omit or send []. Initial Sharer/Viewer grants are not accepted.

allowed_viewersobject

Verified viewers of the approved proposal. API updates add emails and exact domains; omitted entries and empty arrays never remove access. Manage explicit removals in Share. No invitations or staff roles are granted.

allowed_viewers.emailsarray of string (email)

up to 1000 items

allowed_viewers.domainsarray of string

up to 1000 items

ctasobject

{ book_meeting?, accept? }, each { url, label? }, https only. Shown as buttons; clicks come back as proposal.cta_clicked.

ctas.book_meetingobject

A button that books a meeting: { url, label? }, https only.

ctas.acceptobject

A button that accepts the proposal: { url, label? }, https only.

notesstring

Optional staff-written context or instructions, up to 8,000 characters. Notes are not content-screened; the sender controls what is included.

up to 8,000 characters

signersarray of object

Up to 10 { role, name, email, title?, order? }. When a version is promoted it goes out for signature: each signer confirms their email with a code, fills their fields (the kit's "x-signer" fields for their role) and signs on the page.

up to 10 items

signatureobject

{ expires_in_days?, cc? }: days until the signature request expires (30 by default), and up to 10 emails that get the signed copy and nothing else.

signature.expires_in_daysinteger
signature.ccarray of string (email)

up to 10 items

callback_urlrequiredstring (url)

Required HTTPS event endpoint for this proposal. Stored at creation and reused for later events; no workspace default is needed.

matches ^https:\/\/

proposalrequiredobject
proposal.templatestring, or null

One of the kit’s template ids, or null to let the kit choose.

at least 1 characters · up to 80 characters

proposal.inputsrequiredobject

Customer-specific facts validated by the workspace kit. No employee records or identifying health data.

initial_rolesarray of object

Workspace-wide add/upgrade assignments. Omission never revokes access. Normalize email; conflicting roles for one email are invalid. Require an inherited or assigned Editor. Invitations are sent for new/upgraded access.

up to 50 items

request.jsonJSON
{
  "request_id": "portal:deal-123:proposal",
  "callback_url": "https://example.com/webhooks/proposals",
  "source": "portal",
  "recipient": {
    "name": "Acme Logistics"
  },
  "owner": {
    "name": "Dana Ruiz",
    "email": "dana@example.com"
  },
  "initial_roles": [
    {
      "email": "dana@example.com",
      "role": "editor"
    }
  ],
  "proposal": {
    "inputs": {}
  }
}

The job

What GET /jobs/{job_id} returns. Writes return an operation acknowledgment; follow its status_url for the result.

Context-bound jobs include environment and test. In dev and staging, url, live_url and pdf_url are null. After private approval, private_test_url requires authentication and stays pinned to live_version while a newer draft is reviewed. Before approval and in production it is null. Legacy workspaces without an execution context omit these three context fields.

FieldType and notes
environmentenum

The operator-bound customer execution environment. Absent for legacy workspaces without a context.

one of dev, staging, production

testboolean

True for dev and staging private test proposals; false for production.

private_test_urlstring (url), or null

Authenticated private test link pinned to the approved live_version. Null before approval and in production; public URLs remain null in test mode.

matches ^https:\/\/

job_idrequiredstring (uuid)

The job.

request_idrequiredstring

Your id for the proposal, from the request.

statusrequiredenum

queued, building, in_review (a successful draft is available), failed (the first build failed), or withdrawn.

one of queued, building, in_review, failed, withdrawn

urlrequiredstring (url), or null

The public link: what the customer opens. It shows live_version once one is promoted.

matches ^https:\/\/

live_urlrequiredstring (url), or null

The same value as url.

matches ^https:\/\/

review_urlrequiredstring (url), or null

The review link: always the newest successful version, for your team.

matches ^https:\/\/

versionrequiredinteger, or null

The newest version built.

positive

live_versionrequiredinteger, or null

The version on the public link, or null before the first promote.

positive

promoted_atrequiredstring (timestamp), or null

When a version was last promoted.

matches Z$

approved_pdfobject, or null

Authenticated version-bound current approved PDF, with no credential in its URL. Withdrawal retains it until replacement or deletion.

approved_inputsobject, or null

Immutable effective inputs of live_version; null before approval. A newer draft never changes this snapshot.

approved_contractobject, or null
approved_pricing_authorizationobject, or null

Historical pricing authorization, if recorded before the separate authorization step was retired. New approvals use the existing Editor publication flow.

shaperequiredstring, or null

The proposal's shape, as the kit's build chose it.

open_itemsrequiredarray of string

What your team should confirm before it goes live: a section dropped for missing data, a term assumed. Advisory; does not block publication.

revisionsrequiredinteger

How many rounds have run since the first build.

requested_byrequiredstring, or null

The member who asked for it, or null when your systems did.

promoted_byrequiredstring, or null

The member who last promoted it, or null when your systems did.

publishesrequiredobject

Publication history, oldest first, returned as a page.

withdrawn_atrequiredstring (timestamp), or null

When it was withdrawn.

matches Z$

withdrawn_byrequiredstring, or null

The member who withdrew it, or null when your systems did.

roundsrequiredobject

Revision rounds, oldest first, returned as a page.

briefrequiredobject, or null

The deal brief your team reads and edits, once the first build writes it.

errorrequiredstring, or null

Why the last build or round failed.

updated_atrequiredstring (timestamp)

When the job last changed.

matches Z$

pdf_urlrequiredstring (url), or null

The live version's PDF, behind the live link's own gate.

matches ^https:\/\/

review_pdf_urlrequiredstring (url), or null

The newest review version's PDF.

matches ^https:\/\/

engagementrequiredobject, or null

Null before measurable history exists or when analytics are temporarily unavailable; see analytics_status.

needs_attentionrequiredobject, or null
analytics_statusrequiredenum

one of available, not_available, temporarily_unavailable

versionsrequiredobject
deletionrequiredobject, or null
JobJSON
{
  "job_id": "4db5a295-7c1e-4f8a-9d2b-3e6f0a1b2c3d",
  "request_id": "portal:deal-123:proposal",
  "status": "in_review",
  "url": "https://proposals.example.com/acme",
  "live_url": "https://proposals.example.com/acme",
  "review_url": "https://review.pitchunfairly.com/acme",
  "version": 2,
  "live_version": 1,
  "promoted_at": "2026-10-19T14:00:00Z",
  "shape": null,
  "open_items": [],
  "revisions": 1,
  "requested_by": null,
  "promoted_by": null,
  "publishes": {
    "items": [
      {
        "version": 1,
        "at": "2026-10-19T14:00:00Z",
        "by": null
      }
    ],
    "next_url": null
  },
  "withdrawn_at": null,
  "withdrawn_by": null,
  "rounds": {
    "items": [
      {
        "round": 1,
        "kind": "update",
        "at": "2026-10-19T14:00:00Z",
        "by": null,
        "comments": 1
      }
    ],
    "next_url": null
  },
  "brief": null,
  "error": null,
  "updated_at": "2026-10-19T14:00:00Z",
  "pdf_url": null,
  "review_pdf_url": null,
  "engagement": null,
  "needs_attention": null,
  "analytics_status": "not_available",
  "versions": {
    "items": [
      {
        "version": 1,
        "created_at": "2026-10-19T14:00:00Z",
        "review_url": "https://review.pitchunfairly.com/acme?review&v=1",
        "review_pdf_url": null
      },
      {
        "version": 2,
        "created_at": "2026-10-19T14:00:00Z",
        "review_url": "https://review.pitchunfairly.com/acme?review&v=2",
        "review_pdf_url": null
      }
    ],
    "next_url": null
  },
  "deletion": null
}

Event types

A JSON POST to your endpoint whenever a proposal changes or is read, for every proposal in your workspace however it was started: your API, your team in chat, or your CRM. Every API creation request must include callback_url; subsequent events for that proposal go there. The API & webhooks page can set a default for proposals created through other workflows.

State events omit business content; access and engagement events can contain email addresses. Verify signatures before acknowledging unknown types. Open one to see its body.

proposal.in_review
in_review

See the event schema in OpenAPI for its exact fields.

FieldType and notes
event_idrequiredstring (uuid)
operation_idrequiredstring (uuid), or null
operation_statusrequiredenum, or null

one of queued, running, succeeded, failed, cancelled

job_idrequiredstring (uuid)

The job. Null on proposal.ping.

request_idrequiredstring

Your id for the proposal. Null on proposal.ping.

statusrequiredenum

The job status at event creation. Null on proposal.ping.

one of queued, building, in_review, failed, withdrawn

versionrequiredinteger, or null

For engagement, the version observed. Otherwise the newest successfully built version. Null on proposal.ping.

positive

live_versionrequiredinteger, or null

The version on the public link.

positive

revisionsrequiredinteger

How many rounds have run.

urlrequiredstring (url), or null

The public link.

matches ^https:\/\/

review_urlrequiredstring (url), or null

The review link.

matches ^https:\/\/

pdf_urlrequiredstring (url), or null

The live version's PDF.

matches ^https:\/\/

review_pdf_urlrequiredstring (url), or null

The newest review version's PDF.

matches ^https:\/\/

occurred_atrequiredstring (timestamp)

When the event happened. Retries and resends preserve this timestamp.

matches Z$

revision_kindrequiredenum, or null

What started the round that just built; null for the initial build.

one of comments, update

typerequired"proposal.in_review"

The event type.

pricing_changeobject, or null
proposal.in_reviewJSON
{
  "type": "proposal.in_review",
  "event_id": "7f3e9c2a-0000-4000-8000-000000000002",
  "operation_id": null,
  "operation_status": null,
  "job_id": "4db5a295-7c1e-4f8a-9d2b-3e6f0a1b2c3d",
  "request_id": "portal:deal-123:proposal",
  "status": "in_review",
  "version": 2,
  "live_version": 1,
  "revisions": 1,
  "url": "https://proposals.example.com/acme",
  "review_url": "https://review.pitchunfairly.com/acme",
  "pdf_url": null,
  "review_pdf_url": null,
  "occurred_at": "2026-10-19T14:00:00Z",
  "revision_kind": "update",
  "pricing_change": {
    "changed": true,
    "comparison_version": 1,
    "before": {
      "current_price_pepm": 20,
      "discount_note": "Supplied display note"
    },
    "after": {
      "current_price_pepm": 21,
      "discount_note": "Supplied display note"
    }
  }
}
proposal.round_failed
round_failed

See the event schema in OpenAPI for its exact fields.

FieldType and notes
event_idrequiredstring (uuid)
operation_idrequiredstring (uuid), or null
operation_statusrequiredenum, or null

one of queued, running, succeeded, failed, cancelled

job_idrequiredstring (uuid)

The job. Null on proposal.ping.

request_idrequiredstring

Your id for the proposal. Null on proposal.ping.

statusrequiredenum

The job status at event creation. Null on proposal.ping.

one of queued, building, in_review, failed, withdrawn

versionrequiredinteger, or null

For engagement, the version observed. Otherwise the newest successfully built version. Null on proposal.ping.

positive

live_versionrequiredinteger, or null

The version on the public link.

positive

revisionsrequiredinteger

How many rounds have run.

urlrequiredstring (url), or null

The public link.

matches ^https:\/\/

review_urlrequiredstring (url), or null

The review link.

matches ^https:\/\/

pdf_urlrequiredstring (url), or null

The live version's PDF.

matches ^https:\/\/

review_pdf_urlrequiredstring (url), or null

The newest review version's PDF.

matches ^https:\/\/

occurred_atrequiredstring (timestamp)

When the event happened. Retries and resends preserve this timestamp.

matches Z$

revision_kindenum, or null

What started the round that just built.

one of comments, update

typerequired"proposal.round_failed"

The event type.

proposal.round_failedJSON
{
  "type": "proposal.round_failed",
  "event_id": "7f3e9c2a-0000-4000-8000-000000000002",
  "operation_id": null,
  "operation_status": null,
  "job_id": "4db5a295-7c1e-4f8a-9d2b-3e6f0a1b2c3d",
  "request_id": "portal:deal-123:proposal",
  "status": "in_review",
  "version": 2,
  "live_version": 1,
  "revisions": 1,
  "url": "https://proposals.example.com/acme",
  "review_url": "https://review.pitchunfairly.com/acme",
  "pdf_url": null,
  "review_pdf_url": null,
  "occurred_at": "2026-10-19T14:00:00Z"
}
proposal.failed
failed

See the event schema in OpenAPI for its exact fields.

FieldType and notes
event_idrequiredstring (uuid)
operation_idrequiredstring (uuid), or null
operation_statusrequiredenum, or null

one of queued, running, succeeded, failed, cancelled

job_idrequiredstring (uuid)

The job. Null on proposal.ping.

request_idrequiredstring

Your id for the proposal. Null on proposal.ping.

statusrequiredenum

The job status at event creation. Null on proposal.ping.

one of queued, building, in_review, failed, withdrawn

versionrequiredinteger, or null

For engagement, the version observed. Otherwise the newest successfully built version. Null on proposal.ping.

positive

live_versionrequiredinteger, or null

The version on the public link.

positive

revisionsrequiredinteger

How many rounds have run.

urlrequiredstring (url), or null

The public link.

matches ^https:\/\/

review_urlrequiredstring (url), or null

The review link.

matches ^https:\/\/

pdf_urlrequiredstring (url), or null

The live version's PDF.

matches ^https:\/\/

review_pdf_urlrequiredstring (url), or null

The newest review version's PDF.

matches ^https:\/\/

occurred_atrequiredstring (timestamp)

When the event happened. Retries and resends preserve this timestamp.

matches Z$

revision_kindenum, or null

What started the round that just built.

one of comments, update

typerequired"proposal.failed"

The event type.

proposal.failedJSON
{
  "type": "proposal.failed",
  "event_id": "7f3e9c2a-0000-4000-8000-000000000002",
  "operation_id": null,
  "operation_status": null,
  "job_id": "4db5a295-7c1e-4f8a-9d2b-3e6f0a1b2c3d",
  "request_id": "portal:deal-123:proposal",
  "status": "failed",
  "version": null,
  "live_version": null,
  "revisions": 0,
  "url": null,
  "review_url": null,
  "pdf_url": null,
  "review_pdf_url": null,
  "occurred_at": "2026-10-19T14:00:00Z"
}
proposal.promoted
promoted

See the event schema in OpenAPI for its exact fields.

FieldType and notes
event_idrequiredstring (uuid)
operation_idrequiredstring (uuid), or null
operation_statusrequiredenum, or null

one of queued, running, succeeded, failed, cancelled

job_idrequiredstring (uuid)

The job. Null on proposal.ping.

request_idrequiredstring

Your id for the proposal. Null on proposal.ping.

statusrequiredenum

The job status at event creation. Null on proposal.ping.

one of queued, building, in_review, failed, withdrawn

versionrequiredinteger, or null

For engagement, the version observed. Otherwise the newest successfully built version. Null on proposal.ping.

positive

live_versionrequiredinteger, or null

The version on the public link.

positive

revisionsrequiredinteger

How many rounds have run.

urlrequiredstring (url), or null

The public link.

matches ^https:\/\/

review_urlrequiredstring (url), or null

The review link.

matches ^https:\/\/

pdf_urlrequiredstring (url), or null

The live version's PDF.

matches ^https:\/\/

review_pdf_urlrequiredstring (url), or null

The newest review version's PDF.

matches ^https:\/\/

occurred_atrequiredstring (timestamp)

When the event happened. Retries and resends preserve this timestamp.

matches Z$

revision_kindenum, or null

What started the round that just built.

one of comments, update

typerequired"proposal.promoted"

The event type.

proposal.promotedJSON
{
  "type": "proposal.promoted",
  "event_id": "7f3e9c2a-0000-4000-8000-000000000002",
  "operation_id": null,
  "operation_status": null,
  "job_id": "4db5a295-7c1e-4f8a-9d2b-3e6f0a1b2c3d",
  "request_id": "portal:deal-123:proposal",
  "status": "in_review",
  "version": 2,
  "live_version": 1,
  "revisions": 1,
  "url": "https://proposals.example.com/acme",
  "review_url": "https://review.pitchunfairly.com/acme",
  "pdf_url": null,
  "review_pdf_url": null,
  "occurred_at": "2026-10-19T14:00:00Z"
}
proposal.withdrawn
withdrawn

See the event schema in OpenAPI for its exact fields.

FieldType and notes
event_idrequiredstring (uuid)
operation_idrequiredstring (uuid), or null
operation_statusrequiredenum, or null

one of queued, running, succeeded, failed, cancelled

job_idrequiredstring (uuid)

The job. Null on proposal.ping.

request_idrequiredstring

Your id for the proposal. Null on proposal.ping.

statusrequiredenum

The job status at event creation. Null on proposal.ping.

one of queued, building, in_review, failed, withdrawn

versionrequiredinteger, or null

For engagement, the version observed. Otherwise the newest successfully built version. Null on proposal.ping.

positive

live_versionrequiredinteger, or null

The version on the public link.

positive

revisionsrequiredinteger

How many rounds have run.

urlrequiredstring (url), or null

The public link.

matches ^https:\/\/

review_urlrequiredstring (url), or null

The review link.

matches ^https:\/\/

pdf_urlrequiredstring (url), or null

The live version's PDF.

matches ^https:\/\/

review_pdf_urlrequiredstring (url), or null

The newest review version's PDF.

matches ^https:\/\/

occurred_atrequiredstring (timestamp)

When the event happened. Retries and resends preserve this timestamp.

matches Z$

revision_kindenum, or null

What started the round that just built.

one of comments, update

typerequired"proposal.withdrawn"

The event type.

proposal.withdrawnJSON
{
  "type": "proposal.withdrawn",
  "event_id": "7f3e9c2a-0000-4000-8000-000000000002",
  "operation_id": null,
  "operation_status": null,
  "job_id": "4db5a295-7c1e-4f8a-9d2b-3e6f0a1b2c3d",
  "request_id": "portal:deal-123:proposal",
  "status": "withdrawn",
  "version": 2,
  "live_version": 1,
  "revisions": 1,
  "url": "https://proposals.example.com/acme",
  "review_url": "https://review.pitchunfairly.com/acme",
  "pdf_url": null,
  "review_pdf_url": null,
  "occurred_at": "2026-10-19T14:00:00Z"
}
proposal.viewed
viewed

See the event schema in OpenAPI for its exact fields.

FieldType and notes
event_idrequiredstring (uuid)
operation_idrequiredstring (uuid), or null
operation_statusrequiredenum, or null

one of queued, running, succeeded, failed, cancelled

job_idrequiredstring (uuid)

The job. Null on proposal.ping.

request_idrequiredstring

Your id for the proposal. Null on proposal.ping.

statusrequiredenum

The job status at event creation. Null on proposal.ping.

one of queued, building, in_review, failed, withdrawn

versionrequiredinteger, or null

For engagement, the version observed. Otherwise the newest successfully built version. Null on proposal.ping.

positive

live_versionrequiredinteger, or null

The version on the public link.

positive

revisionsrequiredinteger

How many rounds have run.

urlrequiredstring (url), or null

The public link.

matches ^https:\/\/

review_urlrequiredstring (url), or null

The review link.

matches ^https:\/\/

pdf_urlrequiredstring (url), or null

The live version's PDF.

matches ^https:\/\/

review_pdf_urlrequiredstring (url), or null

The newest review version's PDF.

matches ^https:\/\/

occurred_atrequiredstring (timestamp)

When the event happened. Retries and resends preserve this timestamp.

matches Z$

revision_kindenum, or null

What started the round that just built.

one of comments, update

typerequired"proposal.viewed"

The event type.

viewer_emailrequiredstring (email), or null

The viewer it's about.

up to 254 characters · matches ^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$

email_verifiedrequiredboolean

The viewer proved their email with a one-time code.

ctarequiredstring, or null

Which call to action was clicked: book_meeting or accept.

proposal.viewedJSON
{
  "type": "proposal.viewed",
  "event_id": "7f3e9c2a-0000-4000-8000-000000000002",
  "operation_id": null,
  "operation_status": null,
  "job_id": "4db5a295-7c1e-4f8a-9d2b-3e6f0a1b2c3d",
  "request_id": "portal:deal-123:proposal",
  "status": "in_review",
  "version": 2,
  "live_version": 1,
  "revisions": 1,
  "url": "https://proposals.example.com/acme",
  "review_url": "https://review.pitchunfairly.com/acme",
  "pdf_url": null,
  "review_pdf_url": null,
  "occurred_at": "2026-10-19T14:00:00Z",
  "viewer_email": null,
  "email_verified": false,
  "cta": null
}
proposal.cta_clicked
cta_clicked

See the event schema in OpenAPI for its exact fields.

FieldType and notes
event_idrequiredstring (uuid)
operation_idrequiredstring (uuid), or null
operation_statusrequiredenum, or null

one of queued, running, succeeded, failed, cancelled

job_idrequiredstring (uuid)

The job. Null on proposal.ping.

request_idrequiredstring

Your id for the proposal. Null on proposal.ping.

statusrequiredenum

The job status at event creation. Null on proposal.ping.

one of queued, building, in_review, failed, withdrawn

versionrequiredinteger, or null

For engagement, the version observed. Otherwise the newest successfully built version. Null on proposal.ping.

positive

live_versionrequiredinteger, or null

The version on the public link.

positive

revisionsrequiredinteger

How many rounds have run.

urlrequiredstring (url), or null

The public link.

matches ^https:\/\/

review_urlrequiredstring (url), or null

The review link.

matches ^https:\/\/

pdf_urlrequiredstring (url), or null

The live version's PDF.

matches ^https:\/\/

review_pdf_urlrequiredstring (url), or null

The newest review version's PDF.

matches ^https:\/\/

occurred_atrequiredstring (timestamp)

When the event happened. Retries and resends preserve this timestamp.

matches Z$

revision_kindenum, or null

What started the round that just built.

one of comments, update

typerequired"proposal.cta_clicked"

The event type.

viewer_emailrequiredstring (email), or null

The viewer it's about.

up to 254 characters · matches ^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$

email_verifiedrequiredboolean

The viewer proved their email with a one-time code.

ctarequiredstring, or null

Which call to action was clicked: book_meeting or accept.

proposal.cta_clickedJSON
{
  "type": "proposal.cta_clicked",
  "event_id": "7f3e9c2a-0000-4000-8000-000000000002",
  "operation_id": null,
  "operation_status": null,
  "job_id": "4db5a295-7c1e-4f8a-9d2b-3e6f0a1b2c3d",
  "request_id": "portal:deal-123:proposal",
  "status": "in_review",
  "version": 2,
  "live_version": 1,
  "revisions": 1,
  "url": "https://proposals.example.com/acme",
  "review_url": "https://review.pitchunfairly.com/acme",
  "pdf_url": null,
  "review_pdf_url": null,
  "occurred_at": "2026-10-19T14:00:00Z",
  "viewer_email": null,
  "email_verified": false,
  "cta": "book_meeting"
}
proposal.viewer_added
viewer_added

See the event schema in OpenAPI for its exact fields.

FieldType and notes
event_idrequiredstring (uuid)
operation_idrequiredstring (uuid), or null
operation_statusrequiredenum, or null

one of queued, running, succeeded, failed, cancelled

job_idrequiredstring (uuid)

The job. Null on proposal.ping.

request_idrequiredstring

Your id for the proposal. Null on proposal.ping.

statusrequiredenum

The job status at event creation. Null on proposal.ping.

one of queued, building, in_review, failed, withdrawn

versionrequiredinteger, or null

For engagement, the version observed. Otherwise the newest successfully built version. Null on proposal.ping.

positive

live_versionrequiredinteger, or null

The version on the public link.

positive

revisionsrequiredinteger

How many rounds have run.

urlrequiredstring (url), or null

The public link.

matches ^https:\/\/

review_urlrequiredstring (url), or null

The review link.

matches ^https:\/\/

pdf_urlrequiredstring (url), or null

The live version's PDF.

matches ^https:\/\/

review_pdf_urlrequiredstring (url), or null

The newest review version's PDF.

matches ^https:\/\/

occurred_atrequiredstring (timestamp)

When the event happened. Retries and resends preserve this timestamp.

matches Z$

revision_kindenum, or null

What started the round that just built.

one of comments, update

typerequired"proposal.viewer_added"

The event type.

viewer_emailrequiredstring (email)

The viewer it's about.

up to 254 characters · matches ^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$

added_byrequiredstring, or null

Who added them, or asked to.

viarequiredstring, or null

How access originated, such as owner, request, invite or approval. Does not grant a role.

access_request_idrequiredstring (uuid), or null

The access-request UUID, not the proposal request_id.

decisionrequiredenum, or null

What became of the access request; null for unrelated access events.

one of approved, declined, expired, cancelled

expires_atrequiredstring (timestamp), or null

When the request lapses if nobody decides.

matches Z$

byrequiredstring, or null

Who decided or removed.

proposal.viewer_addedJSON
{
  "type": "proposal.viewer_added",
  "event_id": "7f3e9c2a-0000-4000-8000-000000000002",
  "operation_id": null,
  "operation_status": null,
  "job_id": "4db5a295-7c1e-4f8a-9d2b-3e6f0a1b2c3d",
  "request_id": "portal:deal-123:proposal",
  "status": "in_review",
  "version": 2,
  "live_version": 1,
  "revisions": 1,
  "url": "https://proposals.example.com/acme",
  "review_url": "https://review.pitchunfairly.com/acme",
  "pdf_url": null,
  "review_pdf_url": null,
  "occurred_at": "2026-10-19T14:00:00Z",
  "viewer_email": "viewer@example.com",
  "added_by": null,
  "via": null,
  "access_request_id": null,
  "decision": null,
  "expires_at": null,
  "by": null
}
proposal.access_requested
access_requested

See the event schema in OpenAPI for its exact fields.

FieldType and notes
event_idrequiredstring (uuid)
operation_idrequiredstring (uuid), or null
operation_statusrequiredenum, or null

one of queued, running, succeeded, failed, cancelled

job_idrequiredstring (uuid)

The job. Null on proposal.ping.

request_idrequiredstring

Your id for the proposal. Null on proposal.ping.

statusrequiredenum

The job status at event creation. Null on proposal.ping.

one of queued, building, in_review, failed, withdrawn

versionrequiredinteger, or null

For engagement, the version observed. Otherwise the newest successfully built version. Null on proposal.ping.

positive

live_versionrequiredinteger, or null

The version on the public link.

positive

revisionsrequiredinteger

How many rounds have run.

urlrequiredstring (url), or null

The public link.

matches ^https:\/\/

review_urlrequiredstring (url), or null

The review link.

matches ^https:\/\/

pdf_urlrequiredstring (url), or null

The live version's PDF.

matches ^https:\/\/

review_pdf_urlrequiredstring (url), or null

The newest review version's PDF.

matches ^https:\/\/

occurred_atrequiredstring (timestamp)

When the event happened. Retries and resends preserve this timestamp.

matches Z$

revision_kindenum, or null

What started the round that just built.

one of comments, update

typerequired"proposal.access_requested"

The event type.

viewer_emailrequiredstring (email)

The viewer it's about.

up to 254 characters · matches ^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$

added_byrequiredstring, or null

Who added them, or asked to.

viarequiredstring, or null

How access originated, such as owner, request, invite or approval. Does not grant a role.

access_request_idrequiredstring (uuid), or null

The access-request UUID, not the proposal request_id.

decisionrequiredenum, or null

What became of the access request; null for unrelated access events.

one of approved, declined, expired, cancelled

expires_atrequiredstring (timestamp), or null

When the request lapses if nobody decides.

matches Z$

byrequiredstring, or null

Who decided or removed.

proposal.access_requestedJSON
{
  "type": "proposal.access_requested",
  "event_id": "7f3e9c2a-0000-4000-8000-000000000002",
  "operation_id": null,
  "operation_status": null,
  "job_id": "4db5a295-7c1e-4f8a-9d2b-3e6f0a1b2c3d",
  "request_id": "portal:deal-123:proposal",
  "status": "in_review",
  "version": 2,
  "live_version": 1,
  "revisions": 1,
  "url": "https://proposals.example.com/acme",
  "review_url": "https://review.pitchunfairly.com/acme",
  "pdf_url": null,
  "review_pdf_url": null,
  "occurred_at": "2026-10-19T14:00:00Z",
  "viewer_email": "viewer@example.com",
  "added_by": null,
  "via": null,
  "access_request_id": null,
  "decision": null,
  "expires_at": null,
  "by": null
}
proposal.access_decided
access_decided

See the event schema in OpenAPI for its exact fields.

FieldType and notes
event_idrequiredstring (uuid)
operation_idrequiredstring (uuid), or null
operation_statusrequiredenum, or null

one of queued, running, succeeded, failed, cancelled

job_idrequiredstring (uuid)

The job. Null on proposal.ping.

request_idrequiredstring

Your id for the proposal. Null on proposal.ping.

statusrequiredenum

The job status at event creation. Null on proposal.ping.

one of queued, building, in_review, failed, withdrawn

versionrequiredinteger, or null

For engagement, the version observed. Otherwise the newest successfully built version. Null on proposal.ping.

positive

live_versionrequiredinteger, or null

The version on the public link.

positive

revisionsrequiredinteger

How many rounds have run.

urlrequiredstring (url), or null

The public link.

matches ^https:\/\/

review_urlrequiredstring (url), or null

The review link.

matches ^https:\/\/

pdf_urlrequiredstring (url), or null

The live version's PDF.

matches ^https:\/\/

review_pdf_urlrequiredstring (url), or null

The newest review version's PDF.

matches ^https:\/\/

occurred_atrequiredstring (timestamp)

When the event happened. Retries and resends preserve this timestamp.

matches Z$

revision_kindenum, or null

What started the round that just built.

one of comments, update

typerequired"proposal.access_decided"

The event type.

viewer_emailrequiredstring (email)

The viewer it's about.

up to 254 characters · matches ^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$

added_byrequiredstring, or null

Who added them, or asked to.

viarequiredstring, or null

How access originated, such as owner, request, invite or approval. Does not grant a role.

access_request_idrequiredstring (uuid), or null

The access-request UUID, not the proposal request_id.

decisionrequiredenum, or null

What became of the access request; null for unrelated access events.

one of approved, declined, expired, cancelled

expires_atrequiredstring (timestamp), or null

When the request lapses if nobody decides.

matches Z$

byrequiredstring, or null

Who decided or removed.

proposal.access_decidedJSON
{
  "type": "proposal.access_decided",
  "event_id": "7f3e9c2a-0000-4000-8000-000000000002",
  "operation_id": null,
  "operation_status": null,
  "job_id": "4db5a295-7c1e-4f8a-9d2b-3e6f0a1b2c3d",
  "request_id": "portal:deal-123:proposal",
  "status": "in_review",
  "version": 2,
  "live_version": 1,
  "revisions": 1,
  "url": "https://proposals.example.com/acme",
  "review_url": "https://review.pitchunfairly.com/acme",
  "pdf_url": null,
  "review_pdf_url": null,
  "occurred_at": "2026-10-19T14:00:00Z",
  "viewer_email": "viewer@example.com",
  "added_by": null,
  "via": null,
  "access_request_id": null,
  "decision": null,
  "expires_at": null,
  "by": null
}
proposal.viewer_removed
viewer_removed

See the event schema in OpenAPI for its exact fields.

FieldType and notes
event_idrequiredstring (uuid)
operation_idrequiredstring (uuid), or null
operation_statusrequiredenum, or null

one of queued, running, succeeded, failed, cancelled

job_idrequiredstring (uuid)

The job. Null on proposal.ping.

request_idrequiredstring

Your id for the proposal. Null on proposal.ping.

statusrequiredenum

The job status at event creation. Null on proposal.ping.

one of queued, building, in_review, failed, withdrawn

versionrequiredinteger, or null

For engagement, the version observed. Otherwise the newest successfully built version. Null on proposal.ping.

positive

live_versionrequiredinteger, or null

The version on the public link.

positive

revisionsrequiredinteger

How many rounds have run.

urlrequiredstring (url), or null

The public link.

matches ^https:\/\/

review_urlrequiredstring (url), or null

The review link.

matches ^https:\/\/

pdf_urlrequiredstring (url), or null

The live version's PDF.

matches ^https:\/\/

review_pdf_urlrequiredstring (url), or null

The newest review version's PDF.

matches ^https:\/\/

occurred_atrequiredstring (timestamp)

When the event happened. Retries and resends preserve this timestamp.

matches Z$

revision_kindenum, or null

What started the round that just built.

one of comments, update

typerequired"proposal.viewer_removed"

The event type.

viewer_emailrequiredstring (email)

The viewer it's about.

up to 254 characters · matches ^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$

added_byrequiredstring, or null

Who added them, or asked to.

viarequiredstring, or null

How access originated, such as owner, request, invite or approval. Does not grant a role.

access_request_idrequiredstring (uuid), or null

The access-request UUID, not the proposal request_id.

decisionrequiredenum, or null

What became of the access request; null for unrelated access events.

one of approved, declined, expired, cancelled

expires_atrequiredstring (timestamp), or null

When the request lapses if nobody decides.

matches Z$

byrequiredstring, or null

Who decided or removed.

proposal.viewer_removedJSON
{
  "type": "proposal.viewer_removed",
  "event_id": "7f3e9c2a-0000-4000-8000-000000000002",
  "operation_id": null,
  "operation_status": null,
  "job_id": "4db5a295-7c1e-4f8a-9d2b-3e6f0a1b2c3d",
  "request_id": "portal:deal-123:proposal",
  "status": "in_review",
  "version": 2,
  "live_version": 1,
  "revisions": 1,
  "url": "https://proposals.example.com/acme",
  "review_url": "https://review.pitchunfairly.com/acme",
  "pdf_url": null,
  "review_pdf_url": null,
  "occurred_at": "2026-10-19T14:00:00Z",
  "viewer_email": "viewer@example.com",
  "added_by": null,
  "via": null,
  "access_request_id": null,
  "decision": null,
  "expires_at": null,
  "by": null
}
proposal.sent_for_signature
sent_for_signature

See the event schema in OpenAPI for its exact fields.

FieldType and notes
event_idrequiredstring (uuid)
operation_idrequiredstring (uuid), or null
operation_statusrequiredenum, or null

one of queued, running, succeeded, failed, cancelled

job_idrequiredstring (uuid)

The job. Null on proposal.ping.

request_idrequiredstring

Your id for the proposal. Null on proposal.ping.

statusrequiredenum

The job status at event creation. Null on proposal.ping.

one of queued, building, in_review, failed, withdrawn

versionrequiredinteger, or null

For engagement, the version observed. Otherwise the newest successfully built version. Null on proposal.ping.

positive

live_versionrequiredinteger, or null

The version on the public link.

positive

revisionsrequiredinteger

How many rounds have run.

urlrequiredstring (url), or null

The public link.

matches ^https:\/\/

review_urlrequiredstring (url), or null

The review link.

matches ^https:\/\/

pdf_urlrequiredstring (url), or null

The live version's PDF.

matches ^https:\/\/

review_pdf_urlrequiredstring (url), or null

The newest review version's PDF.

matches ^https:\/\/

occurred_atrequiredstring (timestamp)

When the event happened. Retries and resends preserve this timestamp.

matches Z$

revision_kindenum, or null

What started the round that just built.

one of comments, update

typerequired"proposal.sent_for_signature"

The event type.

agreement_idrequiredstring (uuid)

The signature request.

signed_versionrequiredinteger

The version being signed.

positive

signer_rolerequiredstring, or null

The signer's role, as the request named it (proposal.signed and proposal.signature_declined); otherwise null.

signer_emailrequiredstring (email), or null

The signer (proposal.signed and proposal.signature_declined); otherwise null.

up to 254 characters · matches ^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$

signed_countrequiredinteger

How many have signed so far.

signer_countrequiredinteger

How many sign in all.

signed_pdf_urlrequiredstring (url), or null

The signed PDF with its certificate, behind the live link's gate (proposal.signature_completed); otherwise null.

matches ^https:\/\/

proposal.sent_for_signatureJSON
{
  "type": "proposal.sent_for_signature",
  "event_id": "7f3e9c2a-0000-4000-8000-000000000002",
  "operation_id": null,
  "operation_status": null,
  "job_id": "4db5a295-7c1e-4f8a-9d2b-3e6f0a1b2c3d",
  "request_id": "portal:deal-123:proposal",
  "status": "in_review",
  "version": 2,
  "live_version": 2,
  "revisions": 1,
  "url": "https://proposals.example.com/acme",
  "review_url": "https://review.pitchunfairly.com/acme",
  "pdf_url": null,
  "review_pdf_url": null,
  "occurred_at": "2026-10-19T14:00:00Z",
  "agreement_id": "6d1c2f0e-0000-4000-8000-000000000001",
  "signed_version": 2,
  "signer_role": null,
  "signer_email": null,
  "signed_count": 0,
  "signer_count": 2,
  "signed_pdf_url": null
}
proposal.signed
signed

See the event schema in OpenAPI for its exact fields.

FieldType and notes
event_idrequiredstring (uuid)
operation_idrequiredstring (uuid), or null
operation_statusrequiredenum, or null

one of queued, running, succeeded, failed, cancelled

job_idrequiredstring (uuid)

The job. Null on proposal.ping.

request_idrequiredstring

Your id for the proposal. Null on proposal.ping.

statusrequiredenum

The job status at event creation. Null on proposal.ping.

one of queued, building, in_review, failed, withdrawn

versionrequiredinteger, or null

For engagement, the version observed. Otherwise the newest successfully built version. Null on proposal.ping.

positive

live_versionrequiredinteger, or null

The version on the public link.

positive

revisionsrequiredinteger

How many rounds have run.

urlrequiredstring (url), or null

The public link.

matches ^https:\/\/

review_urlrequiredstring (url), or null

The review link.

matches ^https:\/\/

pdf_urlrequiredstring (url), or null

The live version's PDF.

matches ^https:\/\/

review_pdf_urlrequiredstring (url), or null

The newest review version's PDF.

matches ^https:\/\/

occurred_atrequiredstring (timestamp)

When the event happened. Retries and resends preserve this timestamp.

matches Z$

revision_kindenum, or null

What started the round that just built.

one of comments, update

typerequired"proposal.signed"

The event type.

agreement_idrequiredstring (uuid)

The signature request.

signed_versionrequiredinteger

The version being signed.

positive

signer_rolerequiredstring, or null

The signer's role, as the request named it (proposal.signed and proposal.signature_declined); otherwise null.

signer_emailrequiredstring (email), or null

The signer (proposal.signed and proposal.signature_declined); otherwise null.

up to 254 characters · matches ^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$

signed_countrequiredinteger

How many have signed so far.

signer_countrequiredinteger

How many sign in all.

signed_pdf_urlrequiredstring (url), or null

The signed PDF with its certificate, behind the live link's gate (proposal.signature_completed); otherwise null.

matches ^https:\/\/

proposal.signedJSON
{
  "type": "proposal.signed",
  "event_id": "7f3e9c2a-0000-4000-8000-000000000002",
  "operation_id": null,
  "operation_status": null,
  "job_id": "4db5a295-7c1e-4f8a-9d2b-3e6f0a1b2c3d",
  "request_id": "portal:deal-123:proposal",
  "status": "in_review",
  "version": 2,
  "live_version": 2,
  "revisions": 1,
  "url": "https://proposals.example.com/acme",
  "review_url": "https://review.pitchunfairly.com/acme",
  "pdf_url": null,
  "review_pdf_url": null,
  "occurred_at": "2026-10-19T14:00:00Z",
  "agreement_id": "6d1c2f0e-0000-4000-8000-000000000001",
  "signed_version": 2,
  "signer_role": "customer",
  "signer_email": "cfo@acmelogistics.com",
  "signed_count": 1,
  "signer_count": 2,
  "signed_pdf_url": null
}
proposal.signature_completed
signature_completed

See the event schema in OpenAPI for its exact fields.

FieldType and notes
event_idrequiredstring (uuid)
operation_idrequiredstring (uuid), or null
operation_statusrequiredenum, or null

one of queued, running, succeeded, failed, cancelled

job_idrequiredstring (uuid)

The job. Null on proposal.ping.

request_idrequiredstring

Your id for the proposal. Null on proposal.ping.

statusrequiredenum

The job status at event creation. Null on proposal.ping.

one of queued, building, in_review, failed, withdrawn

versionrequiredinteger, or null

For engagement, the version observed. Otherwise the newest successfully built version. Null on proposal.ping.

positive

live_versionrequiredinteger, or null

The version on the public link.

positive

revisionsrequiredinteger

How many rounds have run.

urlrequiredstring (url), or null

The public link.

matches ^https:\/\/

review_urlrequiredstring (url), or null

The review link.

matches ^https:\/\/

pdf_urlrequiredstring (url), or null

The live version's PDF.

matches ^https:\/\/

review_pdf_urlrequiredstring (url), or null

The newest review version's PDF.

matches ^https:\/\/

occurred_atrequiredstring (timestamp)

When the event happened. Retries and resends preserve this timestamp.

matches Z$

revision_kindenum, or null

What started the round that just built.

one of comments, update

typerequired"proposal.signature_completed"

The event type.

agreement_idrequiredstring (uuid)

The signature request.

signed_versionrequiredinteger

The version being signed.

positive

signer_rolerequiredstring, or null

The signer's role, as the request named it (proposal.signed and proposal.signature_declined); otherwise null.

signer_emailrequiredstring (email), or null

The signer (proposal.signed and proposal.signature_declined); otherwise null.

up to 254 characters · matches ^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$

signed_countrequiredinteger

How many have signed so far.

signer_countrequiredinteger

How many sign in all.

signed_pdf_urlrequiredstring (url), or null

The signed PDF with its certificate, behind the live link's gate (proposal.signature_completed); otherwise null.

matches ^https:\/\/

proposal.signature_completedJSON
{
  "type": "proposal.signature_completed",
  "event_id": "7f3e9c2a-0000-4000-8000-000000000002",
  "operation_id": null,
  "operation_status": null,
  "job_id": "4db5a295-7c1e-4f8a-9d2b-3e6f0a1b2c3d",
  "request_id": "portal:deal-123:proposal",
  "status": "in_review",
  "version": 2,
  "live_version": 2,
  "revisions": 1,
  "url": "https://proposals.example.com/acme",
  "review_url": "https://review.pitchunfairly.com/acme",
  "pdf_url": null,
  "review_pdf_url": null,
  "occurred_at": "2026-10-19T14:00:00Z",
  "agreement_id": "6d1c2f0e-0000-4000-8000-000000000001",
  "signed_version": 2,
  "signer_role": null,
  "signer_email": null,
  "signed_count": 2,
  "signer_count": 2,
  "signed_pdf_url": "https://pitchunfairly.com/acme/acme-logistics-1a2b3c/signed.pdf"
}
proposal.signature_declined
signature_declined

See the event schema in OpenAPI for its exact fields.

FieldType and notes
event_idrequiredstring (uuid)
operation_idrequiredstring (uuid), or null
operation_statusrequiredenum, or null

one of queued, running, succeeded, failed, cancelled

job_idrequiredstring (uuid)

The job. Null on proposal.ping.

request_idrequiredstring

Your id for the proposal. Null on proposal.ping.

statusrequiredenum

The job status at event creation. Null on proposal.ping.

one of queued, building, in_review, failed, withdrawn

versionrequiredinteger, or null

For engagement, the version observed. Otherwise the newest successfully built version. Null on proposal.ping.

positive

live_versionrequiredinteger, or null

The version on the public link.

positive

revisionsrequiredinteger

How many rounds have run.

urlrequiredstring (url), or null

The public link.

matches ^https:\/\/

review_urlrequiredstring (url), or null

The review link.

matches ^https:\/\/

pdf_urlrequiredstring (url), or null

The live version's PDF.

matches ^https:\/\/

review_pdf_urlrequiredstring (url), or null

The newest review version's PDF.

matches ^https:\/\/

occurred_atrequiredstring (timestamp)

When the event happened. Retries and resends preserve this timestamp.

matches Z$

revision_kindenum, or null

What started the round that just built.

one of comments, update

typerequired"proposal.signature_declined"

The event type.

agreement_idrequiredstring (uuid)

The signature request.

signed_versionrequiredinteger

The version being signed.

positive

signer_rolerequiredstring, or null

The signer's role, as the request named it (proposal.signed and proposal.signature_declined); otherwise null.

signer_emailrequiredstring (email), or null

The signer (proposal.signed and proposal.signature_declined); otherwise null.

up to 254 characters · matches ^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$

signed_countrequiredinteger

How many have signed so far.

signer_countrequiredinteger

How many sign in all.

signed_pdf_urlrequiredstring (url), or null

The signed PDF with its certificate, behind the live link's gate (proposal.signature_completed); otherwise null.

matches ^https:\/\/

proposal.signature_declinedJSON
{
  "type": "proposal.signature_declined",
  "event_id": "7f3e9c2a-0000-4000-8000-000000000002",
  "operation_id": null,
  "operation_status": null,
  "job_id": "4db5a295-7c1e-4f8a-9d2b-3e6f0a1b2c3d",
  "request_id": "portal:deal-123:proposal",
  "status": "in_review",
  "version": 2,
  "live_version": 2,
  "revisions": 1,
  "url": "https://proposals.example.com/acme",
  "review_url": "https://review.pitchunfairly.com/acme",
  "pdf_url": null,
  "review_pdf_url": null,
  "occurred_at": "2026-10-19T14:00:00Z",
  "agreement_id": "6d1c2f0e-0000-4000-8000-000000000001",
  "signed_version": 2,
  "signer_role": "customer",
  "signer_email": "cfo@acmelogistics.com",
  "signed_count": 1,
  "signer_count": 2,
  "signed_pdf_url": null
}
proposal.signature_expired
signature_expired

See the event schema in OpenAPI for its exact fields.

FieldType and notes
event_idrequiredstring (uuid)
operation_idrequiredstring (uuid), or null
operation_statusrequiredenum, or null

one of queued, running, succeeded, failed, cancelled

job_idrequiredstring (uuid)

The job. Null on proposal.ping.

request_idrequiredstring

Your id for the proposal. Null on proposal.ping.

statusrequiredenum

The job status at event creation. Null on proposal.ping.

one of queued, building, in_review, failed, withdrawn

versionrequiredinteger, or null

For engagement, the version observed. Otherwise the newest successfully built version. Null on proposal.ping.

positive

live_versionrequiredinteger, or null

The version on the public link.

positive

revisionsrequiredinteger

How many rounds have run.

urlrequiredstring (url), or null

The public link.

matches ^https:\/\/

review_urlrequiredstring (url), or null

The review link.

matches ^https:\/\/

pdf_urlrequiredstring (url), or null

The live version's PDF.

matches ^https:\/\/

review_pdf_urlrequiredstring (url), or null

The newest review version's PDF.

matches ^https:\/\/

occurred_atrequiredstring (timestamp)

When the event happened. Retries and resends preserve this timestamp.

matches Z$

revision_kindenum, or null

What started the round that just built.

one of comments, update

typerequired"proposal.signature_expired"

The event type.

agreement_idrequiredstring (uuid)

The signature request.

signed_versionrequiredinteger

The version being signed.

positive

signer_rolerequiredstring, or null

The signer's role, as the request named it (proposal.signed and proposal.signature_declined); otherwise null.

signer_emailrequiredstring (email), or null

The signer (proposal.signed and proposal.signature_declined); otherwise null.

up to 254 characters · matches ^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$

signed_countrequiredinteger

How many have signed so far.

signer_countrequiredinteger

How many sign in all.

signed_pdf_urlrequiredstring (url), or null

The signed PDF with its certificate, behind the live link's gate (proposal.signature_completed); otherwise null.

matches ^https:\/\/

proposal.signature_expiredJSON
{
  "type": "proposal.signature_expired",
  "event_id": "7f3e9c2a-0000-4000-8000-000000000002",
  "operation_id": null,
  "operation_status": null,
  "job_id": "4db5a295-7c1e-4f8a-9d2b-3e6f0a1b2c3d",
  "request_id": "portal:deal-123:proposal",
  "status": "in_review",
  "version": 2,
  "live_version": 2,
  "revisions": 1,
  "url": "https://proposals.example.com/acme",
  "review_url": "https://review.pitchunfairly.com/acme",
  "pdf_url": null,
  "review_pdf_url": null,
  "occurred_at": "2026-10-19T14:00:00Z",
  "agreement_id": "6d1c2f0e-0000-4000-8000-000000000001",
  "signed_version": 2,
  "signer_role": null,
  "signer_email": null,
  "signed_count": 1,
  "signer_count": 2,
  "signed_pdf_url": null
}
proposal.ping
ping

See the event schema in OpenAPI for its exact fields.

FieldType and notes
event_idrequiredstring (uuid)
operation_idrequirednull
operation_statusrequirednull
job_idrequirednull

The job. Null on proposal.ping.

request_idrequirednull

Your id for the proposal. Null on proposal.ping.

statusrequirednull

The job status at event creation. Null on proposal.ping.

versionrequirednull

For engagement, the version observed. Otherwise the newest successfully built version. Null on proposal.ping.

live_versionrequirednull

The version on the public link.

revisionsrequired0

How many rounds have run.

urlrequirednull

The public link.

review_urlrequirednull

The review link.

pdf_urlrequirednull

The live version's PDF.

review_pdf_urlrequirednull

The newest review version's PDF.

occurred_atrequiredstring (timestamp)

When the event happened. Retries and resends preserve this timestamp.

matches Z$

revision_kindenum, or null

What started the round that just built.

one of comments, update

typerequired"proposal.ping"

The event type.

proposal.pingJSON
{
  "type": "proposal.ping",
  "event_id": "7f3e9c2a-0000-4000-8000-000000000002",
  "operation_id": null,
  "operation_status": null,
  "job_id": null,
  "request_id": null,
  "status": null,
  "version": null,
  "live_version": null,
  "revisions": 0,
  "url": null,
  "review_url": null,
  "pdf_url": null,
  "review_pdf_url": null,
  "occurred_at": "2026-10-19T14:00:00Z"
}
proposal.operation_completed
operation_completed

See the event schema in OpenAPI for its exact fields.

FieldType and notes
typerequired"proposal.operation_completed"

The event type.

event_idrequiredstring (uuid)
operation_idrequiredstring (uuid)
operation_statusrequiredenum

one of succeeded, failed, cancelled

job_idrequiredstring (uuid), or null

The job. Null on proposal.ping.

request_idrequiredstring, or null

Your id for the proposal. Null on proposal.ping.

status_urlrequiredstring (url)

matches ^https:\/\/

occurred_atrequiredstring (timestamp)

When the event happened. Retries and resends preserve this timestamp.

matches Z$

proposal.operation_completedJSON
{
  "type": "proposal.operation_completed",
  "event_id": "7f3e9c2a-0000-4000-8000-000000000002",
  "operation_id": "7f3e9c2a-0000-4000-8000-000000000001",
  "operation_status": "succeeded",
  "job_id": "4db5a295-7c1e-4f8a-9d2b-3e6f0a1b2c3d",
  "request_id": "portal:deal-123:proposal",
  "status_url": "https://app.unfairly.ai/api/decks/proposals/operations/7f3e9c2a-0000-4000-8000-000000000001",
  "occurred_at": "2026-10-19T14:00:00Z"
}

Event fields

Fields vary by event type. The compact operation-completed event has its own shape. Use the exact OpenAPI event schemas.

Open an event above for its field table and example. Required fields and nullability are specific to that event.

proposal.promotedJSON
{
  "type": "proposal.promoted",
  "event_id": "7f3e9c2a-0000-4000-8000-000000000002",
  "operation_id": null,
  "operation_status": null,
  "job_id": "4db5a295-7c1e-4f8a-9d2b-3e6f0a1b2c3d",
  "request_id": "portal:deal-123:proposal",
  "status": "in_review",
  "version": 2,
  "live_version": 1,
  "revisions": 1,
  "url": "https://proposals.example.com/acme",
  "review_url": "https://review.pitchunfairly.com/acme",
  "pdf_url": null,
  "review_pdf_url": null,
  "occurred_at": "2026-10-19T14:00:00Z"
}

Verifying signatures

Header Unfairly-Signature: t=<unix seconds>,sha256=<hex>. Verify sha256: HMAC-SHA256 of <t>.<raw body>, lowercase hex, keyed with your signing secret as-is.

  • Split the header on commas into name=value pairs and ignore names you don't know.
  • Accept if any sha256 matches (there can be more than one).
  • Refuse a t more than five minutes from now.
  • Never compare the header as a whole string.

No legacy v1/v2 fallback. Rotation is manually initiated with a 72-hour overlap.

Test vector

Your verifier should accept this.

Verify
import { createHmac, timingSafeEqual } from "node:crypto";

// header: the Unfairly-Signature request header. rawBody: the body exactly as received.
export function verify(header, rawBody, secret /* unfcb_… */) {
  const pairs = header.split(",").map((p) => p.split("="));
  const t = Number(pairs.find(([k]) => k === "t")?.[1]);
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;
  const want = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
  return pairs.some(([k, v]) => k === "sha256" && /^[0-9a-f]{64}$/.test(v ?? "") && timingSafeEqual(want, Buffer.from(v, "hex")));
}
Test vectorText
secret: unfcb_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
t:      1792152000
body:   {"type":"proposal.promoted","job_id":"00000000-0000-0000-0000-000000000001"}
sha256: 53f566149296ee847b9ec6826b27fd20c40751f2958f4eda6518288f8df6c26f

Delivery and retries

  • Any 2xx acknowledges receipt. Verify the signature and durably record the event first. Answer within 10 seconds; process the event after you answer.
  • A network error, timeout, 408, 429 or 5xx is tried again with backoff. Respect valid Retry-After within the 72-hour retry horizon. This horizon is an operational target, not a deployed guarantee. Any other answer is final.
  • Redirects aren't followed. We only call public https hosts.
  • A retry sends the same body with a fresh signature, so the same event can arrive twice: deduplicate by event_id.
  • Retries and resends preserve event_id, occurred_at and the body. Reconcile missed outcomes through operation reads.

Testing

  • ?dry_run=1 or ?dry_run=true on POST /requests runs every check and builds nothing. Try it runs one from this page.
  • The API & webhooks page lists every request we received from your workspace, accepted or refused, with why. Send test event there posts a signed proposal.ping to your URL and shows what it answered.
  • Before your endpoint exists, set a test request's callback_url to the capture URL, then read what arrived.
Capture URLURL
https://app.unfairly.ai/api/decks/proposals/callbacks/capture/<workspace>

Changelog

The API is not versioned in its paths. This target contract includes the breaking changes listed below; coordinate adoption before switching clients. Events and objects may gain fields and new event types, so ignore what you don't know. Anything that would break an integration is announced here first, with a date.

  • Operation acknowledgments, initial Editor/Commenter roles, exact-version publication, paginated reads and expanded analytics.
  • Stable event identity and sha256 signatures; generation costs removed from partner responses.
  • Entries below describe historical behavior, not the target contract.
  • An OpenAPI 3.1 document of the whole API, at /openapi.json.
  • GET /schema: your workspace's whole request as JSON Schema.
  • Viewer access events: proposal.viewer_added, proposal.access_requested, proposal.access_decided and proposal.viewer_removed.
  • POST /jobs/{job_id}/viewers takes decide, audience_domains and invite_policy.
  • Events are signed with their own secret, as v2. v1 is deprecated and stops on 2027-01-15.
  • POST /jobs/{job_id}/withdraw, and proposal.withdrawn.
  • POST /jobs/{job_id}/update.
  • The job carries engagement; proposal.viewed and proposal.cta_clicked.
  • ?dry_run=1 on POST /requests, and the capture URL for testing events.
  • A 256 KB body cap and a per-key rate limit.
  • The job carries requested_by, promoted_by and cost.
  • POST /requests, GET /jobs/{job_id} and POST /jobs/{job_id}/revise.
  • Review and live links: POST /jobs/{job_id}/promote and GET and POST /jobs/{job_id}/viewers.
  • Signed status events.
  • A failed first build retries when its request_id is sent again.