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