Register Webhook
Endpoints to create, list, and remove notification webhooks.
Create Webhook
POST /api/external/webhooksHeaders
| Header | Type | Required | Description |
|---|---|---|---|
Authorization | String | Yes | ApiKey {client_id}:{client_secret} |
Content-Type | String | Yes | application/json |
hmac | String | Yes | HMAC-SHA512 signature of the body (learn more) |
Request Body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
url | String | Yes | -- | URL to receive notifications (HTTPS by default) |
events | Array | Yes | -- | List of events to subscribe to. Must be a non-empty array with at least one valid event from the table below. Omitting the field returns 400 {"errors": {"events": ["can't be blank"]}}. |
secret | String | No | auto-generated | Key for HMAC-SHA256 signing of deliveries. If omitted, a random value is auto-generated. |
description | String | No | null | Free description of the webhook for internal identification |
allow_insecure | Boolean | No | false | Allow HTTP (non-HTTPS) URLs. Data security is the client's responsibility. |
Available events (PIX only — other products are not in scope at Owem today):
| Event | Status body | Description | Dispatch |
|---|---|---|---|
pix.charge.created | created | QR code generated or cash-in initiated | Active |
pix.charge.paid | paid | PIX received and settled | Active |
pix.charge.expired | expired | QR code expired without payment (worker every 5 min) | Active |
pix.charge.cancelled | cancelled | QR code cancelled before payment | Registered, not yet dispatched |
pix.payout.queued | queued | PIX send queued by rate limit (per-merchant ClientLimiter OR BACEN DICT bucket) | Active |
pix.payout.processing | processing | PIX sent, awaiting confirmation | Active |
pix.payout.confirmed | settled | PIX sent and confirmed (terminal) | Active |
pix.payout.failed | rejected | PIX send rejected (terminal) | Active |
pix.payout.returned | returned | Sent PIX was returned | Active |
pix.refund.requested | requested | Refund request received (BACEN infraction); preventive block created on the client balance | Active |
pix.refund.completed | settled / completed | Defense analysis finalized and refund executed (or released) | Active |
pix.return.received | settled | PIX return received (credit) | Active |
pix.infraction.created | ACKNOWLEDGED | PIX infraction reported by counterparty via BACEN DICT | Active |
pix.infraction.resolved | CLOSED / CANCELLED | Infraction resolved (admin close, auto-deny or counterparty cancelled) | Active |
pix.infraction.defense_submitted | defense_submitted | Defense submitted by the merchant | Active |
webhook.test | test | Manual test. Available only via Admin/Merchant portal — there is no External API endpoint to dispatch | Manual dispatch |
pix.charge.cancelled is not yet dispatched
The event is in the valid subscriptions enum, but no production code dispatches this type of notification today (QR code cancellation flow not implemented). You may include it in the events array — the API accepts it — but no notification will arrive at your endpoint until the flow is implemented.
Any event outside this table is rejected
When creating a webhook, validate_events in the backend checks each item against the @valid_events enum (schema Fluxiq.Schemas.Webhooks.Webhook). Unknown events (boleto.paid, account.created, sta.file.*, etc.) return 400 with error events: contains invalid events: .... Before session 163 these names were in the enum as aspirational — they were removed because there were no dispatchers in code.
Payloads for each event
Full payload examples for each event are in Webhook Payloads.
Example
BODY='{"url":"https://yoursite.com/webhook","events":["pix.charge.paid","pix.payout.confirmed"]}'
HMAC=$(echo -n "$BODY" | openssl dgst -sha512 -hmac "$CLIENT_SECRET" | awk '{print $2}')
curl -X POST https://api.owem.com.br/api/external/webhooks \
-H "Authorization: ApiKey $CLIENT_ID:$CLIENT_SECRET" \
-H "Content-Type: application/json" \
-H "hmac: $HMAC" \
-d "$BODY"Success Response (201)
{
"worked": true,
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"url": "https://yoursite.com/webhook",
"events": ["pix.charge.paid", "pix.payout.confirmed"],
"secret": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
"description": null,
"is_active": true,
"created_at": "2026-03-07T15:30:00Z"
}Format of id
The webhook id is a canonical UUID v4 (36 characters with hyphens). Use this value directly in DELETE /api/external/webhooks/:id.
Error Response (422)
{
"worked": false,
"detail": "URL deve utilizar HTTPS"
}HTTPS only by default
The webhook URL must use HTTPS. URLs with HTTP will be rejected unless allow_insecure: true is sent on registration.
Important — Webhook Secret
The secret field returned in the registration response is the key used to sign webhook deliveries (HMAC-SHA256). Store this value securely as soon as you receive it — it is what validates that a notification actually came from Owem Pay.
Do NOT confuse with client_secret:
client_secret= authentication for your API requests (Authorization header)- Webhook
secret= verification of received delivery signatures (X-Owem-Signature header)
If you do not send the secret field at registration, a random value will be auto-generated and returned in the response.
See Webhook Validation for examples of how to verify the signature.
Retrieving the secret later
The secret is returned both in POST /api/external/webhooks (creation) and in GET /api/external/webhooks and GET /api/external/webhooks/:id (query). If you lose the value, simply query the webhook again via GET.
In a future version this behavior may be restricted (display only on creation); we recommend storing the secret in a managed secret (vault, SSM, etc.) at registration time.
HTTP URLs
By default, webhooks require HTTPS to guarantee data security in transit. To use HTTP, send allow_insecure: true on registration.
Attention
HTTP URLs transmit data without encryption. The security and confidentiality of the information exchanged is the client's full responsibility. Owem Pay performs webhook delivery normally but assumes no liability for interception or data leakage on unencrypted connections.
Private URLs are always blocked
Even with allow_insecure: true, URLs pointing to private/internal addresses are rejected:
localhost/127.x.x.x- RFC1918:
10.x.x.x,192.168.x.x,172.16-31.x.x - Internal TLDs:
.local,.internal
Webhooks must point to public URLs accessible over the internet.
List Webhooks
GET /api/external/webhooksHeaders
| Header | Type | Required | Description |
|---|---|---|---|
Authorization | String | Yes | ApiKey {client_id}:{client_secret} |
Example
curl -X GET https://api.owem.com.br/api/external/webhooks \
-H "Authorization: ApiKey $CLIENT_ID:$CLIENT_SECRET"Success Response (200)
Returns an array of objects (not wrapped in {"worked": true}). Each item contains the same fields as registration, including secret.
[
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"url": "https://yoursite.com/webhook",
"events": ["pix.charge.paid", "pix.payout.confirmed"],
"description": null,
"account_id": 10014,
"is_active": true,
"allow_insecure": false,
"status": "active",
"secret": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
"created_at": "2026-03-07T15:30:00",
"updated_at": "2026-03-07T15:30:00"
}
]| Field | Type | Description |
|---|---|---|
id | string (UUID) | Webhook identifier |
url | string | Destination URL |
events | array | Subscribed events |
description | string or null | Optional description |
account_id | integer or null | Associated account. null = webhook global for the API key (if supported) |
is_active | boolean | false = webhook paused, no delivery is dispatched |
allow_insecure | boolean | true = HTTP URLs accepted |
status | string | Derived from is_active — "active" or "inactive" |
secret | string | HMAC-SHA256 key used to sign deliveries. Returned on LIST to allow recovery if the client lost the original value |
created_at / updated_at | string ISO 8601 | Timestamps in UTC, format NaiveDateTime (without Z suffix, e.g., "2026-03-07T15:30:00"). Different from other date fields in webhook payloads (paid_at, returned_at, expired_at) which use DateTime ISO 8601 with trailing Z. Always assume UTC for the webhook object fields |
Remove Webhook
DELETE /api/external/webhooks/:idHeaders
| Header | Type | Required | Description |
|---|---|---|---|
Authorization | String | Yes | ApiKey {client_id}:{client_secret} |
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | String (UUID) | Yes | Webhook ID (UUID v4 returned by the creation endpoint) |
Example
curl -X DELETE https://api.owem.com.br/api/external/webhooks/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
-H "Authorization: ApiKey $CLIENT_ID:$CLIENT_SECRET"Success Response (204)
HTTP 204 No Content — empty body. The webhook was successfully removed; no pending delivery will be dispatched.
First call: 204. Subsequent: 404
The first successful call returns 204 No Content. Subsequent calls with the same id return 404 { "errors": { "not_found": "webhook not found" } } — the webhook was already removed. This is not strict HTTP idempotency (where every call would return 204) — it is the standard DELETE behavior when the resource ceases to exist. Write your integration to accept both 204 and 404 as "the webhook is no longer active".
Error Response (400)
Invalid id format (not UUID):
{
"errors": {
"bad_request": "id must be a valid UUID"
}
}Error Response (404)
Webhook does not exist or does not belong to your account:
{
"errors": {
"not_found": "webhook not found"
}
}