---
title: COS webhooks
slug: apis/webhooks/cos-webhooks
description: Learn how to effectively register and manage Cross River webhooks for products like Customer Management and Instant Payments, ensuring secure and reliable event delivery.
docTags: 
createdAt: 2024-08-08T13:24:27.398Z
---

## Register for COS webhooks

When you work with the following Cross River products you must register for **specific** webhooks:

- Customer Management
- Accounts
- Card Issuing
- Instant Payments
- Card Payments
- International Payments
- ACH
- Wires
- Book Transfer
- Checks
- Crypto

This is different for [Lending hooks](docId\:ZKxAN6DslZSy0tVIrJFiZ) where you register for a set of webhooks.

These events each return a resource object that contains relevant details about the subject of each event. This eliminates the need to poll the API to discover changes. The full event details are included and sent to your system.

The system typically aggregates event objects every 30 seconds.

:::hint{type="warning"}
**IMPORTANT**
Webhook delivery is guaranteed at-least-once. While rare, it's technically possible to receive the same event twice. Your message handlers should account for that edge case and be [idempotent](docId\:xQC2lRsG0xVv_9NMkqxKk).
:::

## Event registration

There are several ways to receive event notifications:

- **Push:&#x20;**&#x57;ebhook events that are pushed to a registered partner endpoint (a callback URL). Any 2XX response is expected in order for it to be considered successful.&#x20;
- **Poll:&#x20;**&#x57;ebhook events that are reported after a partner polls the events API periodically and calling acknowledge API method for each event successfully processed.
- **File:** Events will be sent to the partner via SFTP in a CSV file once per day.

:::hint{type="info"}
The `POST` calls for the registration methods are the same. Make sure to select the correct registration`type`, either `Push` or`Poll`or `File`. We recommend that you use **Push** registration.
:::

### Register for push events

Use `POST /webhooks/v1/registrations` to [register for webhook events](docId\:XPfsj7tlk8KwqJr-bqpY3) which includes defining the callback URLs where event reports should be delivered.

:::hint{type="warning"}
**IMPORTANT**
Your system must respond to each Push event with a 2xx status code to avoid getting suspended.
:::

In this example, your `partnerId` identifies you as the entity requesting webhook registration to the `eventName: Core.Account.Opened`, which is a `Push` registration. The `eventName` is being registered to the `callbackUrl: https://cos.yourcompanysite.com/account-events`.

:::CodeblockTabs
Sample webhook push registration

```json
{
  "partnerId": "8gj76s99-jhso-89as-nsl8-119nss8ch7ab",
  "eventName": "Core.Account.Opened",
  "type": "Push", 
  "callbackUrl": "https://cos.yourcompanysite.com/account-events",
  "authUsername": "{userName}",
  "authPassword": "{password}",
  "format": "Basic"
}
```
:::

:::hint{type="warning"}
**IMPORTANT**
To confirm your callback URL is registered, call `PUT /v1/registrations{id}/ping`. It simulates a webhook event sent to your callback URL but doesn't include any resources. In addition, the `isPing` flag will be set to make it easy to handle the event appropriately.
:::

### Register to poll for events

Use `POST /webhooks/v1/registrations` to [register for webhook events](docId\:XPfsj7tlk8KwqJr-bqpY3)  to poll for an event status.

:::hint{type="info"}
We recommend you poll for events only if you're not able to receive webhooks via Push.
:::

In the example below, your `partnerId` identifies you as the entity requesting registration to be able to poll the `eventName`: `Core.Account.Opened`.&#x20;

:::CodeblockTabs
Sample Poll Registration

```json
{
  "partnerId": "8gj76s99-jhso-89as-nsl8-119nss8ch7ab",
  "eventName": "Core.Account.Opened",
  "type": "Poll",
  "format": "Basic"
}
```
:::

### Poll for events

Call `GET /webhooks/v1/events/poll` to poll for events.

When you poll the system, the events are sent in `status`: `Pending `and every event has to be manually acknowledged.

:::hint{type="success"}
Don't poll more than once every 30 seconds.
:::

In the example below, the `eventName`:`Core.Account.Opened` polls the deposit account `resources`: `https://sandbox.crbcos.com/core/v1/dda/accounts/...` to check the registration status.

:::CodeblockTabs
Poll Event Response

```json
{
      "id": "g98sxnbv-9000-nks7-jjsy-xn6554bv9h10",
      "eventName": "Core.Account.Opened",
      "status": "Pending",
      "partnerId": "8gj76s99-jhso-89as-nsl8-119nss8ch7ab",
      "createdAt": "2022-11-04T17:12:34.806Z",
      "lastAttemptedAt": "2022-11-04T17:12:34.806Z",
      "resources": [
        "https://sandbox.crbcos.com/core/v1/dda/accounts/1234567890",
        "https://sandbox.crbcos.com/core/v1/dda/accounts/1234567891"
      ],
      "isPing": false
}
```
:::

| Attribute         | Description                                                                           |
| ----------------- | ------------------------------------------------------------------------------------- |
| `id`              | ID of the event being polled                                                          |
| `eventName`       | Name of the event being polled                                                        |
| `status`          | When polling for an event, the status will always be **Pending**                      |
| `partnerId`       | Your ID in the Cross River system. This ID is in GUID format.                         |
| `createdAt`       | When the event was created.                                                           |
| `lastAttemtpedAt` | When the last attempt was.                                                            |
| `resources`       | Elements (such as an account number or an event name) that the event is reporting on. |
| `isPing`          | When set to true, it confirms your callback URL is registered.                        |

### Acknowledging an event

:::hint{type="warning"}
Failing to acknowledge an event will cause the` GET /webhooks/v1/events/poll` endpoint to respond with the same information.
:::

Call `POST /webhooks/v1/events/{id}/acknowledge` to acknowledge that you received the event.

Use the `id` you received when you checked the event `status` in this call. When you acknowledge an event, the status changes from `Pending` to `Success`.

## Delete a registration

Use the event registration `id` you received when you polled the event to delete an event registration.

## Registration event status

:::hint{type="warning"}
Partners must monitor their registration statuses periodically to ensure all registrations remain active. It is recommended that the partner implement automated monitoring or check registration status regularly to catch and resolve suspended registrations quickly.
:::

Monitor the status of your webhook registrations. There are several statuses:

- **Active**: Everything is OK.
- **Suspended**: Something went wrong. Several attempts to send events to this registration failed. No further attempts will be made until registration is restarted.
- **Restarted**: We are attempting to restart this registration. An event must be successfully transmitted before the status will transition back to Active.

:::hint{type="info"}
Event that we can't successfully deliver will expire after 7 days.
:::

If the system can't deliver an event to one of your registrations, it makes **three** delivery attempts (one initial attempt plus two retries). **If all three attempts fail** its registration status changes to `Suspended` and we start to queue all your events for your registration. No further attempts are made to deliver previous or future events to this endpoint until your registration returns to an Active status.

:::hint{type="warning"}
If there is an event delivery failure, don't delete a registration and re-register for the same event. This prevents you from retrieving any events that were queued for delivery, as well as any events that fired in the time between deletion and re-registration.
:::

If you get a`Suspended` status, review the logs of recent failed events to identify the issue. When the issue is resolved, restart your registration with the [Restart by ID](docId\:hGk9ybGTXn-XwteVS-WVH) webhook API.

The status transitions to `Restarting`. If we can deliver an event successfully the status returns to `Active`. When the status returns to `Active` we deliver all the queued events from when your registration status was `suspended`. If we can't successfully deliver at least 1 event, the status returns to `suspended`.

:::hint{type="info"}
When a suspended webhook is restarted, all queued webhook notifications are delivered immediately.
:::

## Event status

These are the possible event statuses:

- **Pending:** Event has been created, but no attempt to send it has been made yet.
- **Success:** Event was successfully delivered to the registered URL.
- **Failed:&#x20;**&#x43;ross River could not deliver the event to the registered URL. See the event logs for more detail on the failure reason.

Whenever you receive an event, it is always in `Pending` status and refers to the event status (an event was created but not yet delivered), **and not the resource status**.

If you want to see if webhooks were delivered successfully, call `GET /webhooks/v1/events/{id}`. If the event was delivered successfully you will see `status:Success`.

## Authentication

Our system optionally supports basic authentication on each registration. You can supply an `AuthUsername` and `AuthPassword` which will be base64 encoded and included as an Authorization header on each webhook you receive.

:::hint{type="warning"}
**IMPORTANT**
The preferred security practice is to use webhook signature verification instead of basic authentication.
:::

## Signatures

Every webhook is signed with a standard HMAC with SHA256 hash. This provides an added layer of protection from replay attacks. Without a signature, an attacker could intercept a valid payload and retransmit it.

The signature can be found in the event's request header and includes the event timestamp. Our system generates a timestamp and signature each time we send an event to your endpoint. If our system retries an event after a previous failure, we generate a new signature and timestamp for the new delivery attempt. If a timestamp is subsequently changed, the signature is then invalid.

:::CodeblockTabs
Sample Request Header

```json
cos-signature:t:2019-04-02T11:33:26.6672036-04:00,
v1:{secret}
```
:::

:::hint{type="warning"}
**IMPORTANT**
If you receive an event with an old timestamp we recommend you discard or flag it. The recommended tolerance between timestamp and time of delivery will vary by your application's requirements, but should usually be less than 20 minutes. We do however, recommend special consideration for situations where there has been an extended outage at either party.
:::

- All signatures are version 1 (v1). Any other schemes besides v1 should ignored.
- The signature is a standard HMAC with SHA256 hash.

## Validating the signature

1. Extract the timestamp and signature from the cos-signature header.
2. Concatenate the timestamp with the event body with a period in between the two values (as illustrated below).
3. Compute an HMAC with the SHA256 hash function. Use the signing secret (provided by the Integration Team) as the key and use the string created in step #2 as the message. Also be sure to base64 encode the computed hash value.
4. Compare the hash generated in step #3 with the signature extracted from the cos-signature header. If the hash and signature match, this is a valid request from COS.
5. Compare the timestamp to the time received. If the difference exceeds your tolerance for accepting messages, you can reject the message or flag it for further review.

:::CodeblockTabs
Sample Timestamp and Body Concatenation

```json
2019-04-02T11:33:26.6672036-04:00.{"id":"123","eventName":"Core.Customer.Email.Added","partnerId":"123","createdAt":"2019-04-02T11:33:26-04:00","resources":["core/v1/cm/customers/123"]}
```
:::

:::hint{type="success"}
**Example:**
A Node.js script that shows how a webhook signature would typically be validated.
:::

:::CodeblockTabs
Sample Validation Script

```json
const crypto = require('crypto');

function validateSignature(header, payload, signingSecret) {
                let headerParts = header.split(',');
                let timestamp = headerParts[0].replace('t:', '');
                let signature = headerParts[1].trim().replace('v1:', '');

                let secret_buffer = Buffer.from(signingSecret, 'base64');              
                let hmac = new crypto.createHmac('sha256', secret_buffer);

                let hmac_digest = hmac.update(`${timestamp}.${payload}`, 'binary').digest('base64');
                console.log(hmac_digest);

                return hmac_digest === signature;
}


let payload = '{"id":"e7ead744-d6ff-4521-863d-abab0176f849","eventName":"Core.Transaction.Completed","status":0,"partnerId":"d6b4c661-b38a-46a3-8963-a9a40131eacf","createdAt":"2020-04-28T18:45:14.57-04:00","resources":["core/v1/transactions/6aa7e3b2-3c85-4647-aa6f-abab0176e18b"],"details":[{"transactionId":"6aa7e3b2-3c85-4647-aa6f-abab0176e18b","transactionCode":"Account Transfer","debitSubAccount":"2058112745","debitMasterAccount":"2058112745","debitResult":"OK","creditSubAccount":"2101120877","creditMasterAccount":"2101120877","creditResult":"OK","rail":"Internal","railId":"0","amount":"100"}]}';
let signatureHeader = 't:2020-04-28T18:45:15.6360965-04:00, v1:MvGXdx1O1P8+YjWglbmxAxkrAgVlMglSPpCzsR/Ly/w=';
let secret = 'uVdwwB9HIFZ+5/8nmta5PXu6p1kxZcQmXPCNBRhiVNuKNBhIgth8MvmlD7FYoVfHOmcpHO5QYN/3HHnJ+6TO6Q==';

let result = validateSignature(signatureHeader, payload, secret);

console.log(result);
```
:::

