Webhooks

The Webhook API enables integrators to subscribe to specific events on the Synctera platform. Additionally, this API helps integrators reduce the number of requests to the Synctera platform for resource checks. For example, when a customer swipes a card, the Webhook API, if subscribed, will send a POST request to the predefined URL about the transaction. With webhooks, Synctera will push updates to the integrator instead of the integrator pulling updates.


Understand Webhooks and Events

Creating a webhook defines what events the integrator wants to subscribe to via the event_types field. The following example shows how to subscribe to account updates and customer events. Use the request body of POST /v0/webhooks to create a webhook. enabled_events specifies that this webhook should be invoked whenever an account is updated, or when anything happens to a customer. Once such an event occurs, the Webhook API sends a POST request to https://example.com.

{
  "enabled_events": ["ACCOUNT.UPDATED", "CUSTOMER.*"],
  "is_enabled": true,
  "url": "https://example.com"
}

Most events use the <resource>.[<sub-resource>.]<action> naming convention. For example, ACCOUNT.UPDATED means an account was updated. Resources can have sub-resources: TRANSACTIONS.POSTED.CREATED means to subscribe to changes of the sub-resource TRANSACTION.POSTED. You can also use a wildcard * for convenience purposes. For example, you can use it after a <resource> like CUSTOMER.*, which allows you to receive notifications for all the customer events without explicitly listing all of them. Note that Synctera will continuously add more events, so the webhook will automatically subscribe to any new events added to <resource> and send requests.


Integration Steps

  1. Create a signature secret for request validation
  2. Implement a server to receive webhook requests
  3. Create a webhook for events
  4. Triggering events

1. Create a Signature Secret for Request Validation

To ensure that your incoming webhook requests come from Synctera, you must cryptographically validate each request as it arrives. Synctera will use a shared secret to sign each webhook request before sending it to you, and then you will use the same secret to validate requests. To create a webhook secret, call POST /v0/webhook_secrets with an empty request body:

curl \
  -X POST \
  $baseurl/v0/webhook_secrets \
  -H "Authorization: Bearer $apikey" \
  --data-binary ''

Synctera will respond with the generated secret in the response:

{
  "secret": "{signature_secret}"
}

📘

You must use the same secret to validate all incoming webhook requests.

Secret Replacement

You may want to rotate the secret or immediately replace it for security purposes.

To rotate a secret:

  1. Call PUT /v0/webhook_secrets to deprecate the old secret and generate a new one. Set the is_rolling_secret field to true to generate the new secret without deleting the old secret right away.
curl \
  -X PUT \
  $baseurl/v0/webhooks/secret \
  -H "Authorization: Bearer $apikey" \
  -H 'Content-Type: application/json' \
  --data-binary '
  {
    "is_rolling_secret": true
  }'

📘

The request deletes the last secret in 24 hours, so you will have time to update it.

If you want to delete the last secret immediately without waiting for 24 hours, call DELETE /v0/webhook_secrets?old_secret_only=true

2. Implement a Server to Receive Webhook Requests

The applications that receive webhook requests should be publicly accessible so the Synctera platform can send the webhook request to the URL defined in the webhook resource.

The schema for the webhook request is defined in the OpenAPI spec as webhook_request_object.

Request Headers

The request uses the POST method and contains the following headers:

  • Synctera-Signature - This is the signature of the request. Use the signature secret generated from step 2 to verify the request body.
  • Request-Timestamp - The time when Synctera's platform sent the request, as a POSIX timestamp: seconds since 1970-01-01 00:00:00 UTC.
  • Content-Type - Has the value application/json.

Example Request Body

{
  "id": "8145af83-0423-488f-8799-1c8c0bf8b189",
  "url": "http://example.com",
  "webhook_id": "873f7f9c-3063-4098-adcf-1f3af25286f8",
  "type": "ACCOUNT.UPDATED",
  "event_time": "2022-01-25T11:45:54.485698-05:00",
  "metadata": "test webhook",
  "event_resource": "{\"id\":\"7fef9ad7-67dc-4af0-9ce2-70011131c20c\", \"status\": \"ACTIVE_OR_DISBURSED\" ... }",
  "event_resource_changed_fields": "{\"status\": \"RESTRICTED\"}"
}
  • id - The current event ID
  • url - The URL that you specified in your webhook, also the endpoint you will receive this request.
  • webhook_id - The ID of the webhook that sends out this request
  • type- The event type
  • metadata - The same value as the metadata defines in the webhook
  • event_resource Escaped JSON string representing the <resource> of the event. If the type has <sub-resource>, then the string represents the sub-resource.
  • event_resource_changed_fields Escaped JSON string representing the top level fields that have been updated by the event, containing the value prior to the event. Only update event includes this field.

Resource JSON string example

  • object before change: {"a": 1, "b": 2, "c": 3, "n": {"m": 1}}
  • object after change: {"a": 4, "c": 3, "d": 5, "n": {"m": 1, "p": 2}}

event_resource is just the "object after change" itself.

event_resource_changed_fields is {"a": 1, "b": 2, "d": null, "n": {"m": 1}} because:

  • a has value changed, value in before-change object is 1
  • b is deleted from the object, value in before-change object is 2
  • c is not changed, so it is not included in event_resource_changed_fields
  • d is added to the object, value is null because before-change object does not have it
  • n is a nested object where sub-field p is added. Note that the old value of the entire top level field (n) is included in the old value, and that added sub-fields (e.g. p) are not included.

Therefore, event_resource_changed_fields from the original request body means the account resource has been updated. The account status was changed from RESTRICTED to ACTIVE_OR_DISBURSED.

Request Validation

The Synctera-Signature is generated via HMAC with SHA256 hash and the signature secret as the key. The expected value of the Synctera-Signature is HMAC256({request_timestamp} + '.' + {request_body}, {signature_secret_key}). Your service should validate the header matches what you expect.

Furthermore, Synctera-Signature may include two signature strings delimited by . during the rolling secret period, which the old and new signature secrets generate.

⚠️

To prevent replay attacks, integrators should check that the request time is within 5 minutes of now()

See the example code below in Go to handle signature validation:

func ValidateSignature(secret string, payload []byte, reqTime string, signature string) error {
	// Parse request time
	reqTimeVal, err := strconv.ParseInt(reqTime, 10, 64)
	if err != nil {
		return err
	}

	// Generate the signature
	mac := hmac.New(sha256.New, []byte(secret))
	if _, err := mac.Write([]byte(reqTime + ".")); err != nil {
		return err
	}
	if _, err := mac.Write(payload); err != nil {
		return err
	}
	generatedSignature := hex.EncodeToString(mac.Sum(nil))

	// Verify with the signature header
	for _, curSign := range strings.Split(signature, ".") {
		if generatedSignature != curSign {
			continue
		}
		reqT := time.Unix(reqTimeVal, 0)
		if !reqT.Add(time.Minute * 5).After(time.Now()) {
			return errors.New("signature expired")
		}
		return nil
	}
	return errors.New("invalid signature")
}

Response

The Webhook API expects an HTTP 200 response to indicate that the application processed the request successfully. Any 4xx or 5xx level code will be considered a failure on the application side. The Webhook API will retry the same request with exponential backoff until a successful response is received or 55 hours have passed. Events (successful or failed) are retained for 60 days.

Request timeout

Webhook requests will timeout after 5 seconds. Should a webhook request timeout, Synctera will automatically retry with exponential back off.

3. Create a Webhook Subscription for Events

Call the POST /v0/webhooks endpoint to create a webhook subscription. For example, If you have deployed your new service so it is accessible on the public Internet at https://api.example.com/webhook:

curl \
  -X POST \
  $baseurl/v0/webhooks \
  -H "Authorization: Bearer $apikey" \
  -H 'Content-Type: application/json' \
  --data-binary '
  {
    "url": "https://api.example.com/webhook",
    "description": "random test",
    "enabled_events": ["ACCOUNT.*", "CUSTOMER.UPDATED"],
    "metadata": "nothing",
    "is_enabled": true
  }'
  • url is the endpoint that Synctera's webhook service will send requests to
  • description is a brief string that you use to describe the purpose of this webhook (mainly as a reminder to yourself)
  • enabled_events is the list of events that the webhook should subscribe to
  • metadata is an arbitrary string which will be included in every request body as the field metadata
  • is_enabled means the webhook should send requests for matching events; if false, events will be ignored

Once you create the webhook, it will send requests for any newly triggered events.

Subscribing to a wildcard event, e.g. ACCOUNT.*, will send all webhooks for all events that match that pattern. Note that this can include new event types added after the subscription was created.

📘

Webhook requests are not guaranteed to be real-time calls. In most cases Synctera will send a webhook immediately after an event occurs in most. However, in rare cases the delay could be 15-30 seconds on average. Your app should rely on synchronous responses for time sensitive operations.

📘

Webhook requests may be delivered multiple times, e.g. if Synctera does not receive a 200 response and therefore retries. Your application must handle duplicate requests, e.g. by checking the event ID.

📘

Similarly, webhook requests are not guaranteed to be delivered in order. If you update customer A, then customer B, then customer A again, then those three webhooks are usually delivered in that order. But when errors happen, all bets are off. You should not assume that the state of a resource included in a webhook request is the latest state. If you need the latest state, you should fetch it with an API call back to Synctera.

4. Triggering Events

We highly recommend testing your application to ensure it can receive the webhook request. There are several useful endpoints to test if the application is working correctly.

  • POST /v0/webhooks/trigger fires a mock event on the Synctera platform, which triggers all the webhooks that match the event (specified in the request body) to send out a request. This will help debug the application without having to CRUD on the actual resource. However, note that the webhook response body will NOT contain event_resource in this case.
  • GET /v0/webhooks/<webhook_id>/events/<event_id> returns the event with the history of the request sent, including those failed attempts with the response body.
  • POST /v0/webhooks/<webhook_id>/events/<event_id>/resend will trigger it to send the webhook request with the same event again, without waiting for the next automatic retry attempt.