Orkestia
Blog
Guides

Receive webhooks with a re-query

Point a provider's webhook at a Hook URL, run a composition on each delivery, and act only on what the provider's API confirms. Worked example — Banco Inter Pix and boletos

Some providers deliver webhooks you cannot verify in a composition. Banco Inter is one: it sends no signature on the body and expects the receiver to check its client certificate (mutual TLS). Orkestia's Hook endpoint does not check client certificates.

The safe pattern for such providers is hint + re-query. Treat each delivery only as a signal that something changed. Take the id from it, read that object from the provider's API over your own authenticated connection, and act only on the API's answer. A forged or replayed delivery then changes nothing: it either names an object the API does not know, or one already handled.

provider ──POST──▶ Hook URL ──▶ your composition
                                   1. extract the id(s) from the body
                                   2. re-read each id from the provider API (your credentials)
                                   3. act only on the API answer, idempotent on the id

Use this pattern whenever a provider's webhook carries no signature you can check, or a signature you cannot hold in a composition.

1. Build the composition

The Hook starts your composition with the delivery as input. Declare every field the Hook sends, all optional. A composition refuses unknown input fields, so a missing declaration fails every delivery with Unknown field not in schema.

InputTypeContent
payloadSTRINGRaw request body as text
raw_body_base64STRINGRaw body, base64
headersJSONRequest headers
metadataJSONmethod, path, query_params, content_type, event_uuid, redirect_uuid
event_uuidSTRINGThe Hook event (also the delivery reference)
redirect_uuidSTRINGThe Hook redirect that received it
event_typeSTRINGwebhook
actionSTRINGUsually empty
delivery_refSTRINGSame as event_uuid
sourceSTRINGhook

Then the steps:

  1. Extract the ids with control.text.extract_matches over payload. For Inter Pix use the pattern "endToEndId"\s*:\s*"([^"]+)"; for Inter Cobrança use "codigoSolicitacao"\s*:\s*"([^"]+)". A delivery can carry several items, so expect a list.
  2. Re-read each id from the provider with control.http.request, using the same credential steps as your outbound sync. For Inter that means certificate and key from vault.secret.resolve-many, an OAuth token, then:
    • Pix: GET https://cdpj.partners.bancointer.com.br/pix/v2/pix/{endToEndId}
    • Cobrança: GET https://cdpj.partners.bancointer.com.br/cobranca/v3/cobrancas/{codigoSolicitacao}
  3. Act only on the API answer: amount, status and payer come from the re-read, never from payload. Key every write on the provider id (for example an AppData upsert keyed on endToEndId), so a repeated delivery is a no-op.

Never pass certificates, keys or tokens as literal values in the definition. Resolve them from the Vault at run time.

2. Create the Hook redirect

Run hook.create-redirect with the composition as the dispatch target. Use the composition's lineage ORN (from composition.save), so new versions are picked up without touching the redirect:

{
  "name": "inter-pix",
  "allowed_methods": ["POST"],
  "configuration": {
    "dispatch_workflow_type": "orn:orkestia:composition:<org>:lineage/<lineage_uuid>",
    "dispatch_include_raw_body": true
  }
}

The output's public_url is the path to register with the provider, on https://api.orkestia.dev. Its signed segment is the only secret in the URL. Treat it like a password: to rotate it, create a new redirect, register the new URL, then delete the old one with hook.delete-redirect.

The Hook answers 202 as soon as the delivery is stored, so a slow composition never makes the provider time out.

3. Register the URL with the provider

For Inter, register once per webhook kind with control.http.request, using the same certificate, key and token:

KindCall
Pix receivedPUT /pix/v2/webhook/{chave} with {"webhookUrl": "https://api.orkestia.dev/api/app/hook/input/<signed>"}
Boleto / BolePix (paid, cancelled, expired)PUT /cobranca/v3/cobrancas/webhook with the same body
Payments you makePUT /banking/v2/webhooks/{tipoWebhook} (pix-pagamento, boleto-pagamento)

Test against Inter's sandbox first. Check one delivery arrives and starts your composition before switching to production.

4. Cover missed deliveries

A webhook is an accelerator, not the record. Inter retries a failed delivery four times: at 20, 30, 60 and 120 minutes for Pix and Cobrança, and at 5, 10, 30 and 60 minutes for Banking. After that it gives up.

  • List failed sends with GET /pix/v2/webhook/callbacks (Cobrança: GET /cobranca/v3/cobrancas/webhook/callbacks) and resend Pix ones with POST /pix/v2/webhook/callbacks/retry.
  • Run a scheduled reconcile, for example daily, that lists the day's Pix and Cobrança movements from the API and applies anything the webhook missed. Because writes are keyed on the provider id, the reconcile and the webhook never double-count.

Hardening

The re-query is the security control here. If a provider or an auditor requires the client certificate itself to be checked at the edge, that needs a dedicated receiver that verifies the provider's CA. Ask us; it is not self-service.