Webhooks
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
- Create a signature secret for request validation
- Implement a server to receive webhook requests
- Create a webhook for events
- 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:
- Call
PUT /v0/webhook_secrets
to deprecate the old secret and generate a new one. Set theis_rolling_secret
field totrue
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 valueapplication/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 IDurl
- 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 requesttype
- The event typemetadata
- The same value as themetadata
defines in the webhookevent_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 inbefore-change object
is 1b
is deleted from the object, value inbefore-change object
is 2c
is not changed, so it is not included inevent_resource_changed_fields
d
is added to the object, value is null becausebefore-change object
does not have itn
is a nested object where sub-fieldp
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 todescription
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 tometadata
is an arbitrary string which will be included in every request body as the fieldmetadata
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 containevent_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.
Updated about 1 year ago