Halliday Webhooks API

Register HTTPS endpoints to receive signed notifications when a workflow reaches a terminal state, instead of polling for status. You subscribe to one or more event types per webhook. | Event type | Fires when a workflow's status becomes | | --- | --- | | `WORKFLOW_COMPLETED` | `COMPLETE` | | `WORKFLOW_FAILED` | `FAILED` | All management endpoints live under `/orgs/webhooks` and authenticate with a secret API key (passed as a bearer token) that has webhook access. Publishable keys cannot manage webhooks. **Integration checklist** - Receiver is a public HTTPS endpoint (no private IPs). - Save the `signing_secret` when you create the webhook — it is shown only once. - Verify `X-Halliday-Signature` against the raw body, accepting any of its comma-separated signatures. - Respond `2xx` quickly and do the real work afterward. - Skip deliveries whose `id` you have already handled.

OpenAPI Specification

halliday-webhooks-api-openapi.yml Raw ↑
openapi: 3.1.0
info:
  title: Halliday API V2 Assets Webhooks API
  description: 'API V2 for Halliday''s payment infrastructure, supporting onramps, swaps, and offramps.


    This API provides a unified interface for cryptocurrency payments, allowing developers to:

    - Quote payments across multiple providers

    - Execute payments with onramps, swaps, and offramps

    - Track payment status and history


    ## Authentication


    API key authentication is required for all endpoints.

    '
  version: 2.0.0
  contact:
    name: Contact Halliday
    url: https://halliday.xyz
    email: support@halliday.xyz
servers:
- url: https://v2.prod.halliday.xyz
  description: Base domain
security:
- ApiKeyAuth: []
tags:
- name: Webhooks
  description: 'Register HTTPS endpoints to receive signed notifications when a workflow reaches a terminal

    state, instead of polling for status. You subscribe to one or more event types per webhook.


    | Event type | Fires when a workflow''s status becomes |

    | --- | --- |

    | `WORKFLOW_COMPLETED` | `COMPLETE` |

    | `WORKFLOW_FAILED` | `FAILED` |


    All management endpoints live under `/orgs/webhooks` and authenticate with a secret API key

    (passed as a bearer token) that has webhook access. Publishable keys cannot manage webhooks.


    **Integration checklist**


    - Receiver is a public HTTPS endpoint (no private IPs).

    - Save the `signing_secret` when you create the webhook — it is shown only once.

    - Verify `X-Halliday-Signature` against the raw body, accepting any of its comma-separated signatures.

    - Respond `2xx` quickly and do the real work afterward.

    - Skip deliveries whose `id` you have already handled.

    '
paths:
  /orgs/webhooks:
    post:
      summary: Register a webhook
      description: 'Register an HTTPS endpoint to receive signed event notifications when a workflow reaches a

        terminal state. Halliday delivers each event as an HTTP `POST` to your registered `url`, so the

        endpoint must accept `POST` requests. On success the response includes a `signing_secret` that

        is **only ever returned on creation and rotation** — store it immediately, because it cannot be

        retrieved again from the list endpoint.


        Authenticate with a secret API key that has webhook access. Publishable keys cannot manage webhooks.

        '
      operationId: registerWebhook
      tags:
      - Webhooks
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegisterWebhookRequest'
            example:
              url: https://api.yourapp.com/halliday/webhooks
              label: prod-workflows
              event_types:
              - WORKFLOW_COMPLETED
              - WORKFLOW_FAILED
              auth_header:
                name: X-Webhook-Token
                value: a-shared-secret-token
      responses:
        '200':
          description: Webhook registered successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Webhook'
              example:
                id: 4f2c…
                label: prod-workflows
                url: https://api.yourapp.com/halliday/webhooks
                signing_secret: a1b2c3…
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationErrorResponse'
              example:
                errors:
                - code: invalid_type
                  expected: string
                  path:
                  - label
                  message: 'Invalid input: expected string, received undefined'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                errors:
                - kind: other
                  message: Invalid API key
        '403':
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                errors:
                - kind: other
                  message: Publishable keys cannot manage webhooks
    get:
      summary: List webhooks
      description: 'List the webhooks registered for your org. The signing secret is never included in this response.

        '
      operationId: listWebhooks
      tags:
      - Webhooks
      responses:
        '200':
          description: List of registered webhooks
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListWebhooksResponse'
              example:
                webhooks:
                - id: 4f2c…
                  label: prod-workflows
                  url: https://api.yourapp.com/halliday/webhooks
                  event_types:
                  - WORKFLOW_COMPLETED
                  - WORKFLOW_FAILED
                  auth_header_name: null
                  created_at: '2026-06-20T12:00:00.000Z'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                errors:
                - kind: other
                  message: Invalid API key
    patch:
      summary: Update a webhook
      description: 'Update a registered webhook. `label` identifies which webhook to update and cannot be changed.

        The editable fields are **`url`** and **`auth_header`** (pass `auth_header: null` to

        remove a previously set header). `event_types` is not updatable — to change which events a

        webhook subscribes to, delete it and create a new one. To change the signing secret, use the

        rotate-secret endpoint. A successful update returns `200` with an empty body.

        '
      operationId: updateWebhook
      tags:
      - Webhooks
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateWebhookRequest'
            example:
              label: prod-workflows
              url: https://api.yourapp.com/halliday/webhooks/v2
      responses:
        '200':
          description: Webhook updated successfully (empty body)
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationErrorResponse'
              example:
                errors:
                - code: invalid_type
                  expected: string
                  path:
                  - label
                  message: 'Invalid input: expected string, received undefined'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                errors:
                - kind: other
                  message: Invalid API key
        '404':
          description: Webhook not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                errors:
                - kind: other
                  message: No webhook with label 'prod-workflows' found
    delete:
      summary: Delete a webhook
      description: 'Delete a registered webhook, identified by its `label`.

        '
      operationId: deleteWebhook
      tags:
      - Webhooks
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DeleteWebhookRequest'
            example:
              label: prod-workflows
      responses:
        '200':
          description: Webhook deleted successfully (empty body)
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                errors:
                - kind: other
                  message: Invalid API key
        '404':
          description: Webhook not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                errors:
                - kind: other
                  message: No webhook with label 'prod-workflows' found
  /orgs/webhooks/rotate-secret:
    post:
      summary: Rotate the signing secret
      description: 'Generate a new signing secret for a webhook. The new key is added immediately and signs

        alongside any existing keys, so deliveries carry multiple `v1=` signatures during the overlap

        and you can roll the secret in your verifier without downtime.


        The previous keys are only phased out if you pass `retire_after` (floored to at least 24h from

        now). **Omit `retire_after` and the previous secret stays active indefinitely** — both keep

        signing. The response returns the webhook `id` and the new `signing_secret`; store it now, as

        it is not retrievable later.

        '
      operationId: rotateWebhookSecret
      tags:
      - Webhooks
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RotateSecretRequest'
            example:
              label: prod-workflows
              retire_after: '2026-07-01T00:00:00.000Z'
      responses:
        '200':
          description: New signing secret issued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RotateSecretResponse'
              example:
                id: 4f2c…
                signing_secret: d4e5f6…
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationErrorResponse'
              example:
                errors:
                - code: invalid_type
                  expected: string
                  path:
                  - label
                  message: 'Invalid input: expected string, received undefined'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                errors:
                - kind: other
                  message: Invalid API key
        '404':
          description: Webhook not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                errors:
                - kind: other
                  message: No webhook with label 'prod-workflows' found
components:
  schemas:
    Token:
      type: string
      description: Token identifier in the format "chain:address"
      pattern: ^[a-z]+:0x[a-fA-F0-9]+$
      example: ethereum:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
    ProviderIssue:
      type: object
      required:
      - kind
      - message
      properties:
        kind:
          type: string
          enum:
          - provider
        message:
          type: string
    ListWebhooksResponse:
      type: object
      required:
      - webhooks
      properties:
        webhooks:
          type: array
          items:
            $ref: '#/components/schemas/WebhookSummary'
    Amount:
      type: string
      description: A decimal amount string.
    WebhookEventType:
      type: string
      description: 'Event type a webhook can subscribe to. `WORKFLOW_COMPLETED` fires when a workflow''s status

        becomes `COMPLETE`; `WORKFLOW_FAILED` fires when it becomes `FAILED`.

        '
      enum:
      - WORKFLOW_COMPLETED
      - WORKFLOW_FAILED
    Limits:
      type: object
      properties:
        min:
          type: string
          description: Minimum amount as a decimal string.
        max:
          type: string
          description: Maximum amount as a decimal string.
    DeleteWebhookRequest:
      type: object
      required:
      - label
      properties:
        label:
          type: string
          example: prod-workflows
    WebhookSummary:
      type: object
      description: A registered webhook as returned by the list endpoint. Never includes the signing secret.
      required:
      - id
      - label
      - url
      - event_types
      - auth_header_name
      - created_at
      properties:
        id:
          type: string
          format: uuid
          example: 4f2c8c1a-1b2c-4d3e-8f5a-6b7c8d9e0f12
        label:
          type: string
          example: prod-workflows
        url:
          type: string
          format: uri
          example: https://api.yourapp.com/halliday/webhooks
        event_types:
          type: array
          items:
            $ref: '#/components/schemas/WebhookEventType'
        auth_header_name:
          type:
          - string
          - 'null'
          description: 'Name of the custom delivery auth header, if one is configured (the value is never returned).

            `null` when no auth header is set.

            '
          example: null
        created_at:
          type: string
          format: date-time
          example: '2026-06-20T12:00:00.000Z'
    AmountIssue:
      type: object
      required:
      - kind
      - asset
      - given
      - limits
      - source
      - message
      - reason
      properties:
        kind:
          type: string
          enum:
          - amount
        asset:
          $ref: '#/components/schemas/Asset'
        given:
          $ref: '#/components/schemas/Amount'
        limits:
          $ref: '#/components/schemas/Limits'
        downstream_limits:
          $ref: '#/components/schemas/Limits'
        source:
          type: string
        message:
          type: string
        reason:
          type: string
          enum:
          - TOO_LOW
          - TOO_HIGH
          - NO_VALID_AMOUNT
          - UNKNOWN
    GeolocationIssue:
      type: object
      required:
      - kind
      - message
      properties:
        kind:
          type: string
          enum:
          - geolocation
        message:
          type: string
    Issue:
      oneOf:
      - $ref: '#/components/schemas/AmountIssue'
      - $ref: '#/components/schemas/AmountDownstreamIssue'
      - $ref: '#/components/schemas/FundingIssue'
      - $ref: '#/components/schemas/OwnerIssue'
      - $ref: '#/components/schemas/GeolocationIssue'
      - $ref: '#/components/schemas/ProviderIssue'
      - $ref: '#/components/schemas/PayinMethodIssue'
      - $ref: '#/components/schemas/OtherIssue'
      - $ref: '#/components/schemas/UnknownIssue'
      discriminator:
        propertyName: kind
    Asset:
      type: string
      description: Identifier in the token format ("chain:address") or fiat currency code ("USD")
      example: ethereum:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
    Webhook:
      type: object
      description: 'A registered webhook including its signing secret. Returned only on creation and rotation.

        '
      required:
      - id
      - label
      - url
      - signing_secret
      properties:
        id:
          type: string
          example: 4f2c…
        label:
          type: string
          example: prod-workflows
        url:
          type: string
          format: uri
          example: https://api.yourapp.com/halliday/webhooks
        signing_secret:
          type: string
          description: 'HMAC-SHA256 signing secret. Store it now — it is never returned again from the list endpoint.

            '
          example: a1b2c3…
    WebhookAuthHeader:
      type: object
      description: 'Optional custom HTTP header that Halliday attaches to every delivery to your receiver URL, so your

        endpoint can authenticate inbound deliveries in addition to verifying `X-Halliday-Signature`. The

        `value` is write-only — it is never returned by any endpoint; the list endpoint echoes only the

        header name as `auth_header_name`.

        '
      required:
      - name
      - value
      properties:
        name:
          type: string
          description: Header name Halliday sends on each delivery.
          example: X-Webhook-Token
        value:
          type: string
          description: Header value. Write-only — never returned.
          example: a-shared-secret-token
    FundingIssue:
      type: object
      required:
      - kind
      - token
      - balance
      properties:
        kind:
          type: string
          enum:
          - funding
        token:
          $ref: '#/components/schemas/Token'
        balance:
          type: string
          description: Current token balance at the funding address.
    AmountDownstreamIssue:
      type: object
      required:
      - kind
      - asset
      - given
      - limits
      - source
      - message
      - reason
      properties:
        kind:
          type: string
          enum:
          - amount-downstream
        asset:
          $ref: '#/components/schemas/Asset'
        given:
          $ref: '#/components/schemas/Amount'
        limits:
          $ref: '#/components/schemas/Limits'
        downstream_limits:
          $ref: '#/components/schemas/Limits'
        source:
          type: string
        message:
          type: string
        reason:
          type: string
          enum:
          - TOO_LOW
          - TOO_HIGH
          - NO_VALID_AMOUNT
          - UNKNOWN
    OwnerIssue:
      type: object
      required:
      - kind
      - message
      - mitigation
      properties:
        kind:
          type: string
          enum:
          - owner
        message:
          type: string
        mitigation:
          type: string
          enum:
          - change
          - verify
    RotateSecretRequest:
      type: object
      required:
      - label
      properties:
        label:
          type: string
          example: prod-workflows
        retire_after:
          type: string
          format: date-time
          description: 'Optional. When the previous signing secret(s) should stop signing. Floored to at least 24h

            from now. Omit to keep the previous secret active indefinitely (both keep signing).

            '
          example: '2026-07-01T00:00:00.000Z'
    UnknownIssue:
      type: object
      required:
      - kind
      - message
      properties:
        kind:
          type: string
          enum:
          - unknown
        message:
          type: string
    OtherIssue:
      type: object
      required:
      - kind
      - message
      properties:
        kind:
          type: string
          enum:
          - other
        message:
          type: string
    RotateSecretResponse:
      type: object
      required:
      - id
      - signing_secret
      properties:
        id:
          type: string
          example: 4f2c…
        signing_secret:
          type: string
          description: The new signing secret. Store it now — it is not retrievable later.
          example: d4e5f6…
    ErrorResponse:
      type: object
      required:
      - errors
      properties:
        errors:
          type: array
          items:
            $ref: '#/components/schemas/Issue'
      description: Error response for known errors
    UpdateWebhookRequest:
      type: object
      required:
      - label
      properties:
        label:
          type: string
          description: Identifies the webhook to update. Cannot be changed.
          example: prod-workflows
        url:
          type: string
          format: uri
          description: Optional. New receiver URL. Must be HTTPS on port 443.
          example: https://api.yourapp.com/halliday/webhooks/v2
        auth_header:
          description: 'Optional. Set or replace the custom delivery auth header, or pass `null` to remove an

            existing one.

            '
          anyOf:
          - $ref: '#/components/schemas/WebhookAuthHeader'
          - type: 'null'
    RegisterWebhookRequest:
      type: object
      required:
      - url
      - label
      - event_types
      properties:
        url:
          type: string
          format: uri
          description: 'Receiver URL. Must be HTTPS on port 443. Private/reserved IP addresses are rejected (SSRF protection).

            '
          example: https://api.yourapp.com/halliday/webhooks
        label:
          type: string
          description: 'Human-readable identifier, unique per org. Used to address the webhook on update, rotate, and delete.

            '
          example: prod-workflows
        event_types:
          type: array
          minItems: 1
          description: At least one event type to subscribe to.
          items:
            $ref: '#/components/schemas/WebhookEventType'
          example:
          - WORKFLOW_COMPLETED
          - WORKFLOW_FAILED
        auth_header:
          $ref: '#/components/schemas/WebhookAuthHeader'
        signing_secret:
          type: string
          description: 'Optional. Provide your own HMAC-SHA256 signing secret instead of letting Halliday generate

            one. Omit this to receive a generated `signing_secret` in the response.

            '
          example: my-own-signing-secret
    ValidationErrorResponse:
      type: object
      description: 'Returned when a request body fails schema validation. `errors` contains one entry per

        validation issue.

        '
      required:
      - errors
      properties:
        errors:
          type: array
          items:
            type: object
            required:
            - code
            - path
            - message
            properties:
              code:
                type: string
                description: Validation issue code (for example `invalid_type`).
                example: invalid_type
              expected:
                type: string
                description: Expected type. Present on type-mismatch issues.
                example: string
              path:
                type: array
                description: Path to the offending field in the request body.
                items:
                  type:
                  - string
                  - integer
                example:
                - label
              message:
                type: string
                example: 'Invalid input: expected string, received undefined'
    PayinMethodIssue:
      type: object
      required:
      - kind
      - message
      properties:
        kind:
          type: string
          enum:
          - payin_method
        message:
          type: string
  securitySchemes:
    ApiKeyAuth:
      type: http
      scheme: bearer
      bearerFormat: API_KEY