Send Test Webhook
Requires the manage:webhooks scope.
Send a sample event to the subscription's callback URL and report what it answered.
The sample is signed and shaped exactly like a real delivery, so the
response tells a partner whether their signature verification works before
any real event depends on it. The payload describes a synthetic user and is
flagged is_test, so it must never be processed as a real event. Delivery is
one attempt with no retries.
- subscription_id
- Opaque id of the subscription to send the sample to.
The delivery outcome, carrying the callback URL's status code when it answered.
- HTTPException
- 400 Bad Request when the callback URL no longer resolves to a public address, or 404 Not Found when the financial institution has no enabled subscription with that id.
Authorization
APIKeyHeader Partner API key, required on every endpoint except "GET /health". Your financial institution issues and revokes these keys from the InvestiFi Ops Center. Each key carries an environment-identifying prefix such as "ifi_prod_" plus a fixed set of scopes, so a valid key still returns 403 Forbidden on an endpoint whose scope it was not granted; the response "detail" names the check that failed.
In: header
Path Parameters
Response Body
application/json
application/json
application/json
curl -X POST "https://partner-api.investifi.com/v1/webhooks/subscriptions/string/test"{
"delivered": true,
"response_status_code": 0,
"event_id": "string"
}{
"detail": "Key does not have the required scope: manage:webhooks."
}{
"detail": [
{
"loc": [
"string"
],
"msg": "string",
"type": "string",
"input": null,
"ctx": {}
}
]
}KYC Status Change Event
Delivers a user's identity verification status change to your callback URL. Sent to every callback URL subscribed to the `kyc` event type when a user's status changes for one of your financial institution's investing products. Intermediate states carrying no outcome you can act on are not delivered. One subscription covers every delivered status: filter on `data.kyc_status` (`pending`, `document_requested`, `approved`, `rejected`, `closed`, `account_takeover`) rather than subscribing per status. A user who has finished onboarding into a product reports `approved` for that product, named in `data.product`. Each request carries two headers: - `X-InvestiFi-Webhook-Id`: the delivery's idempotency key, equal to `event_id` and reused across retries. Deduplicate on it. - `X-InvestiFi-Webhook-Signature`: `t=<unix_seconds>,v1=<hex digest>`. The digest is a hash-based message authentication code (HMAC-SHA256) over the timestamp, a period, and the raw request body, keyed by the signing secret returned from your first subscribe. `v1` is a scheme tag: match on it rather than assuming one value. The header is sent once your financial institution has a signing secret. Verify against the raw request body, before parsing it, so the bytes match what was signed. Compare digests in constant time: ```python import hashlib import hmac parts = dict(p.split("=", 1) for p in signature_header.split(",")) signed = f"{parts['t']}.{raw_body.decode()}".encode() expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest() if not hmac.compare_digest(expected, parts["v1"]): raise ValueError("Signature does not match") ``` Delivery is at-least-once. Return any 2xx status to acknowledge; a non-2xx response or a timeout is retried. `links.self` points at the authoritative user record for consumers that would rather refetch than trust the payload. Each subscription is pinned to the API version it was created through, and `links.self` is built at that version: a subscription created via the `v1` subscribe endpoint emits `/v1/...` links, and keeps doing so after newer API versions ship. When you migrate to a new API version, switch both at the same time: point your API calls at the new base URL and re-create your webhook subscriptions through the new version's subscribe endpoint, then delete the old subscriptions.
Transaction Completed Event
Delivers a completed transaction to your callback URL. Sent to every callback URL subscribed to the `transaction` event type when an order of one of your financial institution's users fills: the trade completed and the user's holdings and cash balance changed. Other lifecycle movements (creation, pending states, cancellations, failures) are not delivered. A managed-portfolio (`robo` product) deposit, withdrawal, or rebalance delivers one webhook for the whole transaction once it completes, not one per buy/sell leg it placed. `data.transaction_id` names that managed-portfolio transaction; the legs remain separate records in the transaction feed. `data.status` uses the same vocabulary the transaction endpoints report and always carries `completed` today. Filter on `data.status` rather than assuming every delivery is a completion, so new statuses can be delivered in the future without breaking your consumer. The transaction is named by the `data.transaction_id` the transaction endpoints resolve. The payload deliberately carries no balances, positions, quantities, or amounts. On receipt, refetch what you need: `links.self` points at the authoritative transaction record, and the Get User Detail endpoint returns the user's refreshed account values (for example to update a balance shown in your app after the fill). Each request carries two headers: - `X-InvestiFi-Webhook-Id`: the delivery's idempotency key, equal to `event_id` and reused across retries. Deduplicate on it. - `X-InvestiFi-Webhook-Signature`: `t=<unix_seconds>,v1=<hex digest>`. The digest is a hash-based message authentication code (HMAC-SHA256) over the timestamp, a period, and the raw request body, keyed by the signing secret returned from your first subscribe. `v1` is a scheme tag: match on it rather than assuming one value. The header is sent once your financial institution has a signing secret. Verify against the raw request body, before parsing it, so the bytes match what was signed. Compare digests in constant time: ```python import hashlib import hmac parts = dict(p.split("=", 1) for p in signature_header.split(",")) signed = f"{parts['t']}.{raw_body.decode()}".encode() expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest() if not hmac.compare_digest(expected, parts["v1"]): raise ValueError("Signature does not match") ``` Delivery is at-least-once. Return any 2xx status to acknowledge; a non-2xx response or a timeout is retried.

