Webhooks

In addition to polling for the response to be available, Fonoa supports Webhooks to notify you once your request has been processed and the results are available.

Request

POST /webhook-url
Content-Type: application/json
Fonoa-Webhook-Token: eyJhbGciOiJSUzI1NiIsImtpZCI6ImRhYWI3ZmY3LTAzN2UtNGI0Yy04NDY3LWQ3OThiODhhOWQwNiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsid2ViaG9vayJdLCJleHAiOjE3NDM1MTYxNjEsImlhdCI6MTc0MzUxMjU2MSwiaXNzIjoiaHR0cHM6Ly9maXQtcGFzc3BvcnQuZm9ub2EuY29tIiwianRpIjoiMDFKUVJRNFQxOFdaSzNLSDNZMkJBNzA1WlgiLCJuYmYiOjE3NDM1MTI1NjEsInBhc3Nwb3J0LmZvbm9hLmNvbS92MS9pZGVudGl0eSI6eyJmaXRTZXJ2aWNlIjp7ImlkIjoibWVnYWZvbm9hLWphenoiLCJuYW1lIjoibWVnYWZvbm9hIiwiZW52IjoiamF6eiIsInRlYW0iOiJkYXRhIn19LCJwYXNzcG9ydC5mb25vYS5jb20vdjEvaW5mbyI6eyJzaGEyNTZfY2hlY2tzdW0iOiJiYzJjZjM5NDRkNzkxZjgyN2Q5Y2NmNDJmNGNlOWIwMTY1YzliMmVkOTllYWI5ZTViYjRlYjhiMGY2NjQ3MWU2In0sInN1YiI6ImZvbm9hOmZpdFNlcnZpY2U6ZW52PWpheno6bmFtZT1tZWdhZm9ub2E6aWQ9bWVnYWZvbm9hLWphenoifQ.bFPFM4emNbyA-By9qXg64tQq4RYmQ27o0lSK12TTJzaqcHKOHq6DoMc7ZUldTTPHjF-WFSV5NzI_fPuGeMASxl4dgMiqdics0eaf_5cuo3-FS7u5pkT99dyXVvCVuPF00U94rAn-baky1QIkAsyKLBnan9Zu_V7zj3TotpBzJ2HGEGpySzlCzO00giNzmiwy7T2F54EOvxVvK8eaekzmVy3fmLda3aNoaO1YEGo6ouSxRKGEP01_7kXi_PL9b26ikaahM1z8s_1te5JWnQxHIYEIbheovYik6_7lnhStbZV0Tg0kkhPNrKdDYEym_g5jDoc8GdfUl28ZpujkNgQdRA

{
    "event_type": "lookup.batch_validation_completed",
    "delivered_at": "2006-01-02T15:04:05Z",
    "resource_id": "95afe365-7e3f-40e8-91fa-b3a4fd08eb68",
    "resource_url": "https://api.fonoa.com/lookup/v2/batch-validations/95afe365-7e3f-40e8-91fa-b3a4fd08eb68",
    "webhook_id": "875bd24499d303cbe8afb3db1987d8aa522d63bb"
}
Field nameDescription
event_typeThe event at Fonoa that triggered the Webhook. Usually this is a "completed" event for an operation.
delivered_atTimestamp of when Fonoa tried sending the Webhook. The format is RRC3339 and ISO8601 compatible, strictly in the format YYYY-MM-DDTHH:mm:ssZ and always in UTC timezone.
resource_idID of the resource that triggered the event.
resource_urlURL at which the full resource can be retrieved.
webhook_idIdempotency key for this webhook event. This needs to be treated as opaque string.

Getting started

To get started with using Webhooks, we recommend to first use our demo environment. Since usually the handling of incoming Webhhooks requests differs based on the event type, instead of providing a test event, we recommend to test all events the API client needs by triggering such an event (e.g. by calling the "Request a batch validation" endpoint to test "batch validation completed" events).

Configuration

We currently do not offer an automatic way of configuring Webhooks. Instead, please notify your contact at Fonoa that you want to start using Webhooks and let us know: - For which event(s) should Webhooks be configured - Which HTTPS endpoint the Webhooks should be delivered to. This can be either one URL for all event types, or one URL per event type.

Best practices

Idempotency

Fonoa does not normally make multiple requests for the same event on a resource, but does not guarantee only-once delivery of Webhooks. In addition, on HTTP error responses and timeouts (see retries section), the same notification is re-sent multiple times.

It is up to the API customer to decide on how to handle repeated webhook calls, but we do provide a field webhook_id in the payload that can be used as an idempotency key, as we guarantee the field to not change on retried notifications.

Webhook validation

A third-party attacker with knowledge of the API client's Webhook URL(s) could attempt to send spoofed request to trick the client into accepting wrong data. Clients should therefore validate incoming webhooks. They should also verify the resource_id matches a resource that the API client is aware of.

As Fonoa is running on cloud infrastructure, we do not provide a list of outgoing IP addresses.

Webhook tokens

Token validation is a method of webhook validation which does not rely on a shared secret but using short lived tokens. In each webhook notification we add a header Fonoa-Webhook-Token. This token contains an OIDC compliant JWT which is publicly verifiable using any OIDC library configured to https://fit-passport.fonoa.com.

The fit passport URL provides public keys which the OIDC library fetches and uses to verify the signature in the JWT. Creating the signature is only possible with the private keys which are kept secret, ensuring that if the signature is valid, the token has come from us. Additionally, the tokens are only valid for 1 hour. This ensures that even if the token is leaked, it will quickly expire.

Once you have validated the JWT, you can use the JWT claims. You should verify the aud (audience) claim is webhook. For each webhook, we set a claim in passports.fonao.com/v1/info, sha256_checksum. This is the SHA256 checksum (encoded to hex) of the HTTP request body. You can take the SHA256 checksum of the request body and compare it to the checksum we provide in the validated JWT. If they match then the contents of the webhook are verified to come from us and have not been tampered with.

{
  "aud": [
    "webhook"
  ],
  "exp": 1742924697,
  "iat": 1742921097,
  "iss": "https://fit-passport.fonoa.com",
  "jti": "01JQ732R61KCJ3JYT8NAX360WY",
  "nbf": 1742921097,
  "passport.fonoa.com/v1/identity": {
    "fitService": {
      "env": "jazz",
      "id": "megafonoa-jazz",
      "name": "megafonoa",
      "team": "data"
    }
  },
  "passport.fonoa.com/v1/info": {
    "sha256_checksum": "991da5c925fe369d6cdf192371c8cf53600ec743599439a227231734cdaf3d10"
  },
  "sub": "fonoa:fitService:env=jazz:name=megafonoa:id=megafonoa-jazz"
}

For example, in Go, a client can use the go-oidc library to validate a token:

// Connect to the Fonoa token public OIDC endpoint
provider, err := oidc.NewProvider(ctx, "https://fit-passport.fonoa.com")
if err != nil {
    return err
}

// Create the webhook token verifier
verifier := provider.VerifierContext(ctx, &oidc.Config{ClientID: "webhook"})

...

// Verify the token from the HTTP header
token, err := verifier.Verify(ctx, r.Header.Get("Fonoa-Webhook-Token"))
if err != nil {
    // Token is invalid
    return err
}

// Get the token claims
claims := Claims{}
err := token.Claims(&claims)
if err != nil {
    return err
}

// Get the checksum of the request body
checksum := sha256.Sum256(body)

// Convert the body checksum to hex
checksumEncoded := hex.EncodeToString(checksum[:])

// Compare the checksums
if claims.SHA256Checksum == checksumEncoded {
    // Checksums match, webhook is valid
} else {
    // Checksums don't match, webhook is invalid
}

Errors and retries

Should a Webhook notification be undeliverable, either due to a connection error, or when receiving a HTTP response code in the HTTP Client Error (4xx) or HTTP Server Error (5xx) range, the notification will be retried with an exponential back-off. Retries will contain the same webhook_id (see Idempotency), but a different delivered_at timestamp.

Webhook requests have a timeout of 30 seconds.

Should an API client be unreachable for extended period of time, webhook notification might get discarded after multiple retries. It is recommended that an API client implements reconciliation functionality for those events by falling back to the polling approach for resources for which no webhook has been successfully received.

Redirects

If a request returns a HTTP Redirect (3xx) response code and a Location is specified, the redirect is followed up to 3 levels deep. Redirects to non-secure URLs are not followed. Should a redirect target return an error (see Errors and retries), the retry will again go to the configured URL.