Skip to content

Testing Webhooks Without Real Money

Webhooks are the primary mechanism for keeping your platform in sync with GIB, and onboarding requires webhook endpoints stable and signature verification working before live processing begins (see Getting Started → Phase 3). This guide shows how to exercise the full webhook path, endpoint, signature, event handling, and delivery monitoring, without moving a cent: with the testWebhook operation, the simulate and test submission flags, and the sandbox environment.

For the webhook data model (subscriptions, event types, delivery semantics, retries), see the Webhooks concept page.

Why test webhooks

Three things can break independently, and each needs its own test:

  1. Reachability and signature. Your endpoint must accept GIB's HTTP POST, verify the Signature header, and return a 2XX quickly. testWebhook exercises exactly this, with no Transaction involved.
  2. Event handling. Your handler must parse real event payloads and drive your internal state correctly. Sandbox submissions and test-flagged Transactions produce real lifecycle events end to end.
  3. Production configuration. Production subscriptions, secrets, and endpoints are separate from sandbox and need their own verification before launch. testWebhook covers this without any transaction activity.

Prerequisites

The code snippets in this guide require the following variables:

  • BASE_API_URL - The target GIB API domain, which can be found in the API-Reference.
  • API_KEY - The API key setup by the client for using the API. Can be generated using the Dashboard.

You also need at least one webhook subscription. Create one with Webhook Subscription: Create: name, url (must use https), and event_type_filter (a regex matched against event types) are required, and the response includes the subscription's signing secret (secret_key_sha256_b64).

Sandbox vs. production webhook configuration

GIB runs two fully separated environments (see Getting Started → Environments). Sandbox uses synthetic data, simulated rail processing, and accelerated state transitions, which makes it the right place for end-to-end event testing.

Environment Dashboard API base URL
Sandbox https://sandbox-dashboard.gibank-api.com/ https://sandbox-base-api.gibank-api.com
Production https://dashboard.gibank-api.com/ https://base-api.gibank-api.com

What the separation means for webhooks:

  • Subscriptions exist per environment. Because the environments are fully separated, a subscription configured in sandbox does not exist in production. Configure, and test, each environment on its own.
  • Signing secrets differ. Each subscription has its own secret. A deployment serving both environments must verify each delivery with the secret of the subscription that sent it.
  • Sandbox is rebuilt on a regular cadence. Subscriptions are sandbox state like everything else. Script their creation rather than configuring them by hand, so a rebuild costs you minutes.
  • Delivered payloads identify the sender. Every delivered payload carries a stack_title field (Production or Sandbox), so a handler that receives traffic from the wrong environment can detect and reject it.

Verify your endpoint with testWebhook

testWebhook (POST /webhook/test) triggers a webhook test event on demand. You choose an event type; GIB finds every subscription of your Company whose event_type_filter regex matches it and delivers a payload to each matching URL. No Transaction, Batch, or other resource is created.

Field Required Purpose
event_type Yes The event type to announce, from the standard event-type enum (e.g. outbound_ach_transaction_created). Matched against your subscriptions' filters.
body Yes An object delivered back to your endpoint as the payload's body.
direct_invoke No true delivers synchronously and returns any delivery error in the response. Otherwise the normal queue with retry and backoff is used.
incl_non_active No true also triggers subscriptions with active: false.
url = f"{BASE_API_URL}/webhook/test"

payload = {
    "event_type": "outbound_ach_transaction_created",
    "body": {"ping": "from testWebhook"},
    "direct_invoke": True,
}

headers = {
    "Content-type": "application/json",
    "Authorization": f"{API_KEY}"
}

response = requests.request("POST", url, json=payload, headers=headers)
response.raise_for_status()

for trigger in response.json()["trigger"]:
    print(trigger["webhook_subscription"]["id"], trigger["error"])

The response lists every subscription that matched and was triggered. With direct_invoke: true, each entry's error tells you whether the delivery succeeded (null) or what went wrong; with the queued path, error is always null and you check the outcome in the event trace instead. An empty trigger array means no subscription's filter matched your event_type, which is itself a useful test of your filter regex.

Shape the test body like the real thing

The body you send is delivered as-is. A real .*_transaction_.* event carries the full serialized Transaction under a transaction key (see the payload schema in API reference → Webhooks), so if you want your handler's parsing logic to run during the test, mirror that structure:

payload = {
    "event_type": "outbound_book_transaction_posted",
    "body": {
        "transaction": {
            "id": 12345,
            "type": "book",
            "state": "posted",
            "amount": 700,
            "test": True,
        }
    },
    "direct_invoke": True,
}

Verify a production endpoint before going live

Subscriptions can be created with active: false. Inactive subscriptions receive no event traffic, but testWebhook with incl_non_active: true still delivers to them. That gives you a safe production bring-up sequence:

  1. Create the production subscription with active: false.
  2. Call testWebhook with incl_non_active: true and direct_invoke: true until delivery and signature verification both pass.
  3. Activate the subscription with Webhook Subscription: Update (active: true).

No transaction activity is needed at any point.

Simulate transaction events end to end

testWebhook proves your endpoint works, but not that lifecycle events flow correctly when a Transaction moves through its states. For that, use the two boolean flags accepted by the Transaction-creation endpoints (see Transactions → Submission options):

Flag What is stored Webhooks Use it for
simulate Nothing (request is validated, then rolled back) None Validating request payloads and your submit code
test A real Transaction, flagged test: true Normal lifecycle events End-to-end webhook and state-machine testing

Dry-run validation with simulate

With simulate: true, the request runs through full validation and returns the normal response shape, but nothing is persisted: the returned Transaction objects carry id: -1 and batch_id: -1. This example uses the setup from Create a BOOK Transfer from Subledger:

url = f"{BASE_API_URL}/transaction/from-subledger"

payload = {
    "type": "book",
    "origin_subclient_account_id": SUBCLIENT_ACCOUNT_ID_A,
    "destination_subclient_account_id": SUBCLIENT_ACCOUNT_ID_B,
    "amount": 700,  # In cents
    "external_key": "WEBHOOK-TEST-0001",
    "test": False,
    "simulate": True,
}

headers = {
    "Content-type": "application/json",
    "Authorization": f"{API_KEY}"
}

response = requests.request("POST", url, json=payload, headers=headers)
response.raise_for_status()

Two simulate behaviors to design around:

  • No webhooks fire. Webhook deliveries are queued only after a successful database commit. A simulation is rolled back, so its deliveries are discarded; do not wait for events from a simulated submission.
  • Duplicates do not fail the request. Duplicate detection runs, but the 409 ConflictError is only raised for real submissions. Detected duplicates are reported in the X-Duplicate-Transaction-Transfer-Keys response header instead, so check it: a clean simulation does not guarantee the real submission cannot conflict.

End-to-end events with test

With test: true (and simulate: false), GIB stores a real Transaction that walks the normal state machine and emits the normal {direction}_{type}_transaction_{state} webhook events. The record stays flagged everywhere it appears: in API responses, in transaction search (the listing and search endpoints accept a test query filter), and inside the transaction object of each webhook payload, so reporting and reconciliation can keep it segregated from real activity.

When a test Batch reaches the step that would upload its file toward rail processing, GIB skips the upload and advances the Batch and its Transactions to the next state instead. The state changes emit the corresponding webhook events, and no file leaves the platform. A return originated from a test Transaction inherits the flag, so return events can be exercised the same way.

A minimal end-to-end loop in sandbox:

  1. Create (or script) a subscription with an event_type_filter such as .*_book_transaction_.*.
  2. Submit the transfer above with test: true and simulate: false.
  3. Watch your endpoint receive the {direction}_book_transaction_{state} events as the Transaction advances. Sandbox state transitions are accelerated, so the sequence arrives quickly.
  4. Confirm every received payload passes signature verification, and that your handler is idempotent: delivery is at-least-once (see Webhooks → Delivery semantics and Idempotency).

Watch deliveries in the event trace

Every subscription keeps a per-delivery trace. Webhook Subscription: Get Events (GET /webhook/subscription/{webhook_subscription_id}/event) shows which events have been sent and what your endpoint answered, which is how you debug queued testWebhook calls and missing lifecycle events. The same trace is visible in the dashboard.

Signature verification during testing

Test deliveries are real deliveries: they are signed exactly like production events, with an HMAC-SHA256 computed over the raw JSON request body using the subscription's secret, delivered as a hex digest in the Signature header. Run your production verification code against testWebhook deliveries; if it passes there, the code path is correct.

import base64, hmac, hashlib

def verify(subscription_secret_b64: str, raw_body: bytes, signature_header: str) -> bool:
    secret = base64.b64decode(subscription_secret_b64)
    expected = hmac.new(secret, raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature_header)

Two rules that bite during testing:

  • Verify the raw bytes. The signature is computed over the exact JSON string GIB sends. If your framework parses the JSON and you re-serialize it before hashing, key order or whitespace differences will fail verification even though the payload is authentic. Capture the raw request body.
  • Use the right secret per environment. Sandbox and production subscriptions have different secrets. A handler serving both environments must select the secret by subscription; the payload carries webhook_subscription_id and stack_title for exactly this.

Common pitfalls

  • Waiting for webhooks after a simulate submission. Simulations are rolled back before any delivery is queued; no events will arrive.
  • Persisting IDs from a simulation. The returned IDs are -1 by design; nothing exists to reference.
  • Trusting a clean simulation to rule out conflicts. Duplicate detection does not raise 409 ConflictError under simulate; check the X-Duplicate-Transaction-Transfer-Keys response header.
  • Testing only with direct_invoke: true. Direct invocation bypasses the queue. The production path is queued, with retries and backoff; finish with at least one queued test and confirm it in the event trace.
  • Slow handlers. A delivery counts as failed unless your endpoint returns a 2XX within 10 seconds, and failures trigger retries. Acknowledge first, process asynchronously. See Webhooks → Delivery semantics.
  • Hand-configured sandbox subscriptions. Sandbox is rebuilt on a regular cadence, taking your subscriptions with it. Script subscription setup so a rebuild is a non-event.
  • Forgetting the test flag in production experiments. A production submission without test: true is a real submission. If you must verify anything in production, prefer testWebhook first, and test: true for anything that creates a Transaction.

Resources

For more information on the endpoints used in this guide, see these API docs:

Related pages: