---
title: "Webhook API"
description: "The inbound webhook on a form: endpoint, bearer secret, JSON body with subject_id and fields, idempotency key, the two responses, the once-per-employee rule and every code the log can record."
sidebar:
  order: 3
area: flows
feature: forms
appPath: /flows/home
lastVerified: 2026-10-22
version: "2026-10-16"
audience: admin
related:
  - flows/forms/create-a-webhook
  - flows/forms/reference-form-field-types
  - notices
intercom:
  - id: "16858360"
    title: Create a Webhook (Pyn 2)
    url: https://intercom.help/pyn/en/articles/16858360-create-a-webhook-pyn-2
    note: draws on
---

A webhook belongs to one form. Another system posts a JSON body to it with a bearer secret; Pyn accepts the payload, then separately turns it into a submission on the form for the employee it names, logging the outcome on the webhook. [Create a webhook](https://support.pynhq.com/flows/forms/create-a-webhook/) covers the setup in the app.

## Request

```
POST https://<your Pyn host>/api/webhook/<webhookId>
Authorization: Bearer <secret>
Content-Type: application/json
Idempotency-Key: <any string>      (optional)

{
  "subject_id": "E1234",
  "fields": {
    "jobTitle": "Account Executive",
    "startedOnboarding": true,
    "firstWeekRating": 4
  }
}
```

- `webhookId` and the secret come from the form's **Settings** drawer. The secret is shown once when minted.
- `subject_id` is the employee's id in the source system chosen under **Subject** on the form (for example a BambooHR Employee ID). Pyn finds the employee holding that id from that system; the same id from another system does not match.
- `fields` holds one entry per form field, named by the field's alias (its label in camel case) or its key. Every key must name a field on the form, and every value must be one the field can hold: a string for **Text**, `true` or `false` for **Checkbox**, an integer 1 to 5 for **Likert Scale**, an employee id for **Buddy** and **Mentor**. **Content** fields take no value.
- The body must be a JSON object. Nesting values under `fields` keeps field names from colliding with `subject_id`.

## Responses

| Status | Meaning |
| --- | --- |
| `202` | Accepted. The payload was taken and will be processed; this does not mean the submission succeeded. Read the log. |
| `404` | Rejected: the id is malformed or unknown, the webhook is archived, or the bearer secret does not match. The cases are not distinguished, so the endpoint cannot be probed. |

A request that reaches the endpoint with a body that is not JSON, or during a fault on Pyn's side, is answered with the usual `4xx` or `5xx`; a `5xx` can be retried.

## Idempotency

Send an `Idempotency-Key` header and a repeat of the same key to the same webhook is acknowledged with `202` and processed no further. The key is hashed with the webhook's id, so the same key sent to a different webhook is a separate delivery. Without the header every post is processed. The key itself is not stored; the log's `eventId` is the handle for tracing a delivery.

## Processing rules

- A webhook submits for an employee at most once. Once a subject's submission is complete, later payloads for the same `subject_id` are dropped with `already-submitted`. A form that needs repeat answers from one person cannot use a webhook for them.
- A payload is taken whole or not at all: a missing required field or a value of the wrong type drops the whole payload rather than recording a partial submission.
- A form with no **Subject** set accepts nothing (`no-subject`).
- An accepted submission behaves like any other: it appears in the form's **Activity** drawer and export, writes Buddy or Mentor values to the employee, and starts any automation with a **Form Submission** trigger on the form.

## Log codes

Every payload leaves one entry in the webhook's log (the form's **Settings** drawer, with a CSV export of the whole log).

| Code | Outcome | Meaning |
| --- | --- | --- |
| `submitted` | Processed | Recorded a submission for the identifier. |
| `no-subject` | Dropped | The form has no webhook subject configured. |
| `invalid-payload` | Dropped | The body did not parse as `{ subject_id, fields }`; the message names the field and reason. |
| `unknown-subject` | Dropped | No employee holds that identifier in the chosen source system. |
| `already-submitted` | Dropped | This webhook has already submitted for that identifier. |
| `ambiguous-fields` | Dropped | A name matched more than one field, or a field was named twice (by key and alias). |
| `missing-fields` | Dropped | A required field was not supplied. |
| `invalid-fields` | Dropped | A key names no field on the form, or a value is the wrong type for its field. |

Each dropped payload also raises a notice in [Notices](https://support.pynhq.com/notices/), keyed by its code, so a caller repeating one mistake updates a single notice.

## Secrets

| Action | Effect |
| --- | --- |
| **Create Secret** | Mints the secret for a webhook that has none. A webhook without a secret accepts nothing. |
| **Replace Secret** | Mints a new secret; the old one stops working immediately. |

Secrets are shown once and cannot be read back. Both actions need an Admin.

## Related

- [Create a webhook](https://support.pynhq.com/flows/forms/create-a-webhook/) for the steps in the app.
- [Form field types](https://support.pynhq.com/flows/forms/reference-form-field-types/) for the values each field accepts.
- [Notices](https://support.pynhq.com/notices/) for where dropped payloads surface.

## Related Intercom articles

- [Create a Webhook (Pyn 2)](https://intercom.help/pyn/en/articles/16858360-create-a-webhook-pyn-2) — draws on
