> For the complete documentation index, see [llms.txt](https://docs.tiny.plus/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.tiny.plus/api/endpoints/webhooks.md).

# Webhooks

A webhook sends a record to your URL when someone presses a **button** on it in tiny+. An account adds buttons to its record types (as custom fields); your integration subscribes a URL to one, and each press delivers the record. This is how Zapier's tiny+ triggers work.

Adding and removing webhooks needs an **account admin's** token.

## How it fits together

1. List the account's buttons with [`GET /v2/buttons`](/api/endpoints/account-structure.md#buttons), and pick one.
2. Subscribe your URL to it with [`POST /v2/webhooks/add`](#subscribing).
3. Each time someone presses that button on a record, tiny+ `POST`s the record to your URL — see [What your URL receives](#what-your-url-receives).
4. When you're done, [unsubscribe](#unsubscribing) with the id `add` gave you.

## Subscribing

<mark style="color:green;">`POST`</mark> `https://api.tiny.plus/v2/webhooks/add`

```bash
curl -X POST "https://api.tiny.plus/v2/webhooks/add" \
  -H "Authorization: Bearer {{user_access_token}}" \
  -H "Content-Type: application/json" \
  -d '{
        "hook_url": "https://example.com/tiny-hook/Qk7s2x",
        "type": "button",
        "id": "11183"
      }'
```

```json
{
    "id": "161137",
    "url": "https://example.com/tiny-hook/Qk7s2x",
    "message": "Webhook subscription successful!"
}
```

| Field      | Description                                                                  |
| ---------- | ---------------------------------------------------------------------------- |
| `hook_url` | Required. The URL to deliver to. It must start with `http://` or `https://`. |
| `type`     | Required. Always `button`.                                                   |
| `id`       | Required. The button's id, from `GET /v2/buttons`.                           |

Keep the `id` in the response: it's the subscription's id, which you need to unsubscribe. It isn't the button's id.

{% hint style="info" %}
Subscribing the same URL twice makes two subscriptions, and each press is then delivered twice. Keep track of what you've subscribed.
{% endhint %}

## Testing

<mark style="color:blue;">`GET`</mark> `https://api.tiny.plus/v2/webhooks/test?id={{button_id}}`

Returns a sample of what a delivery for that button looks like: the most recently changed record of the button's type that the token's user can see, with its related records. Use it to map fields before any real press.

The sample comes back as an array holding one record, where a real delivery sends the record on its own.

## What your URL receives

A `POST` with a JSON body: the record whose button was pressed, with its fields, custom fields and tags, and its linked records under `related`.

```json
{
    "id": "8",
    "flavour": "companies",
    "name": "Brookfield Multiplex",
    "record_url": "https://crm.tiny.plus/company/8",
    "created_date": "2026-09-30 09:29:17.000",
    "custom_11183": "2026-09-29 23:29:48.000",
    "tags": [],
    "related": {
        "contacts": [ { "id": "4521", "name": "Alex Amorosi" } ]
    }
}
```

* `related` is an object of record type to records, or `[]` when there are none.
* The button's own field (here `custom_11183`) holds the time of the **previous** press.

{% hint style="warning" %}
**Date-times in a delivery aren't in UTC**, unlike the rest of the API. The record's own date-times are shifted to the time zone of the person who pressed the button (Australia/Sydney when there isn't one). Linked records under `related`, and custom date fields, stay in UTC.
{% endhint %}

### Delivery behaviour

* **Deliveries aren't signed**, and carry no secret. To check a request really came from your subscription, put an unguessable token in the `hook_url` you subscribe, as in the example above, and check for it.
* **tiny+ waits for your URL to answer** before the button press finishes, so answer quickly and do slow work afterwards.
* **There are no retries.** A delivery that fails isn't sent again, and the subscription stays active.

## Unsubscribing

<mark style="color:green;">`POST`</mark> `https://api.tiny.plus/v2/webhooks/unsubscribe`

```bash
curl -X POST "https://api.tiny.plus/v2/webhooks/unsubscribe" \
  -H "Authorization: Bearer {{user_access_token}}" \
  -H "Content-Type: application/json" \
  -d '{ "id": "161137" }'
```

```json
{
    "message": "Unsubscription successful."
}
```

`id` is the subscription id that `add` returned.

## Errors

| Request       | Case                                     | Response                                                                   |
| ------------- | ---------------------------------------- | -------------------------------------------------------------------------- |
| `add`         | Not an account admin                     | `403 {"error": "Only an account admin can add a webhook."}`                |
| `add`         | A field missing                          | `400 {"error": "Missing required field - hook_url"}` (or `- type`, `- id`) |
| `add`         | `type` isn't `button`                    | `400 {"error": "Type not implemented."}`                                   |
| `add`         | `id` isn't a button                      | `404 {"error": "Could not load button."}`                                  |
| `add`         | `hook_url` isn't a web address           | `400 {"error": "Give a web address starting with http:// or https://."}`   |
| `unsubscribe` | `id` missing or not a number             | `400 {"error": "Invalid id provided."}`                                    |
| `unsubscribe` | No such subscription, or already removed | `404 {"error": "Trigger not found."}`                                      |
| `unsubscribe` | Not an account admin                     | `403 {"error": "You may not delete this webhook."}`                        |
| `test`        | `id` isn't a button                      | `404 {"error": "Could not load button."}`                                  |

`GET /v2/webhooks` doesn't list subscriptions; there's no endpoint for listing them.
