# Webhooks

This guide is intended for developers looking to integrate real-time subscription event notifications into their applications. With our webhook system, you’ll receive automated POST requests to your designated URL for events like new sign-ups, renewals, and payment failures.

## **Getting Started**

A **webhook** allows our system to send real-time updates 📡 to your application. We’ll POST a JSON payload to your specified endpoint whenever a relevant subscription event occurs.

### **Step 1: Create/Edit Subscription & Plug in Your Webhook URL**

You can set up a webhook URL in the Flash web app either while creating a new subscription or by editing an existing subscription:

1. When **creating** a new subscription:
   1. Head over to **New Subs** > **Create a Subscription Plan**
   2. Make sure to click on the “**Use Advanced Webhook Features”** checkbox to make the Webhook Url field appear
   3. Input your **Webhook URL** in the form when creating the subscription<br>

      <figure><img src="/files/d4imEWmuIxXC1NB4oPNq" alt=""><figcaption></figcaption></figure>
   4. After creating the Subscription, you will receive a url with the subscription and the subscription key. You can use this key to decode and verify the JWT token that comes with our event (see guide below).
2. When you have **already created** a subscription:
   1. Head over to My Subscriptions
   2. Select the subscription you want to add webhooks for
   3. Click on “Edit Subscription”
   4. Fill in/modify the **Webhook Url** field
   5. The subscription key is also visible in this screen and can be used to decode and verify the JWT token that comes with our event (see guide below).<br>

      <figure><img src="/files/3D2Zbny3Nz1m46YfBSAM" alt=""><figcaption></figcaption></figure>

### **Step 2: Configure Your Webhook Endpoint**

To receive webhook notifications, set up a POST endpoint in your application:

1. Ensure it accepts JSON payloads.
2. Process the data based on the event type and take appropriate actions.

**Security Tip:** Always use HTTPS to secure webhook transmissions.

#### **Responding to Webhook Calls**

Your webhook endpoint should respond with a `200 OK` status to acknowledge receipt of the event. If no response or an error is received, we may retry the notification.

***

## **Event Types**

The system triggers the following events, each sending a POST request with event data to your webhook:

<details>

<summary>1. User Signed Up (name='user_signed_up', id=1)</summary>

Triggered when a user subscribes to a plan. The payload includes details like the user's public key, plan, and transaction info.

```json
{
  "data": {
    "public_key": "55a12716a6c4e8c95fc83dc046c3ea2209d3e3a1b87b15c48ef562b5a8599ed8",
    "external_uuid": "adf8cab9-add3-400d-b83c-9b68cfb6be8d",
    "name": "Jane Doe",
    "email": "jane.doe@example.com",
    "about": "Enthusiast of digital art",
    "picture_url": "<https://example.com/jane.jpg>",
    "user_plan": "Premium",
    "user_plan_id": "23",
    "signup_date": "2024-02-15",
    "next_payment_date": "2024-03-15",
    "failed_payment_date": "",
    "transaction_id": "654",
    "transaction_amount": "100",
    "transaction_currency": "Satoshis"
    "transaction_date": "2024-02-15"
  }
}
```

</details>

<details>

<summary>2. Renewal Successful (name='renewal_successful', id=2)</summary>

Sent when a user's subscription renewal is successfully processed.

```json
{
  "data": {
    "public_key": "55a12716a6c4e8c95fc83dc046c3ea2209d3e3a1b87b15c48ef562b5a8599ed8",
    "external_uuid": "adf8cab9-add3-400d-b83c-9b68cfb6be8d",
    "name": "Jane Doe",
    "email": "jane.doe@example.com",
    "about": "Enthusiast of digital art",
    "picture_url": "<https://example.com/jane.jpg>",
    "user_plan": "Premium",
    "user_plan_id": "24"
    "signup_date": "2024-02-15",
    "next_payment_date": "2024-04-15",
    "failed_payment_date": "",
    "transaction_id": "837",
    "transaction_amount": "100",
    "transaction_currency": "Satoshis",
    "transaction_date": "2024-03-15"
  }
}
```

</details>

<details>

<summary>3. Renewal Failed (name='renewal_failed', id=3)</summary>

Sent if a subscription renewal fails, typically due to a payment issue.

```json
{
  "data": {
    "public_key": "55a12716a6c4e8c95fc83dc046c3ea2209d3e3a1b87b15c48ef562b5a8599ed8",
    "external_uuid": "adf8cab9-add3-400d-b83c-9b68cfb6be8d",
    "name": "Jane Doe",
    "email": "jane.doe@example.com",
    "about": "Enthusiast of digital art",
    "picture_url": "<https://example.com/jane.jpg>",
    "user_plan": "Premium",
    "user_plan_id": "67",
    "signup_date": "2024-02-15",
    "next_payment_date": "",
    "failed_payment_date": "2024-03-15",
    "transaction_id": "-1",
    "transaction_amount": "100",
    "transaction_currency": "Satoshis",
    "transaction_date": ""
  }
}
```

</details>

<details>

<summary>4. Subscription Paused (name='user_paused_subscription', id=4)</summary>

Sent if a subscription renewal fails, typically due to a payment issue.

```json
{
  "data": {
    "public_key": "55a12716a6c4e8c95fc83dc046c3ea2209d3e3a1b87b15c48ef562b5a8599ed8",
    "external_uuid": "adf8cab9-add3-400d-b83c-9b68cfb6be8d",
    "name": "Jane Doe",
    "email": "jane.doe@example.com",
    "about": "Enthusiast of digital art",
    "picture_url": "<https://example.com/jane.jpg>",
    "user_plan": "Premium",
    "user_plan_id": "67",
    "signup_date": "2024-02-15",
    "next_payment_date": "",
    "failed_payment_date": "2024-03-15",
    "transaction_id": "-1",
    "transaction_amount": "100",
    "transaction_currency": "Satoshis",
    "transaction_date": ""
  }
}
```

</details>

<details>

<summary>5. Subscription Cancelled (name='user_cancelled_subscription', id=5)</summary>

Sent if a subscription renewal fails, typically due to a payment issue.

```json
{
  "data": {
    "public_key": "55a12716a6c4e8c95fc83dc046c3ea2209d3e3a1b87b15c48ef562b5a8599ed8",
    "external_uuid": "adf8cab9-add3-400d-b83c-9b68cfb6be8d",
    "name": "Jane Doe",
    "email": "jane.doe@example.com",
    "about": "Enthusiast of digital art",
    "picture_url": "<https://example.com/jane.jpg>",
    "user_plan": "Premium",
    "user_plan_id": "67",
    "signup_date": "2024-02-15",
    "next_payment_date": "",
    "failed_payment_date": "2024-03-15",
    "transaction_id": "-1",
    "transaction_amount": "100",
    "transaction_currency": "Satoshis",
    "transaction_date": ""
  }
}
```

</details>

***

## Webhook Payload Structure

The data sent to your webhook URL will be structured as a JSON object containing two primary fields: **`eventType`** and **`data`**. The **`eventType`** field indicates the type of event, while the **`data`** field contains event-specific information.

<details>

<summary>Here's a breakdown of the data you can expect for each event type:</summary>

* **Common Fields Across All Event Types:**
  * **`public_key`**: The user's public key.
  * **`name`**: The user's name.
  * **`email`**: The user's email address.
  * **`about`**: A brief description of the user.
  * **`picture_url`**: The URL to the user's profile picture.
  * **`user_plan`**: The subscription plan the user has signed up for.
  * **`user_plan_id`**: The subscription plan id the user has signed up for.
* **Event-Specific Fields:**
  * **`signup_date`**: The date the user signed up (only for **`user_signed_up`**).
  * **`next_payment_date`**: The next payment date for the subscription (only for **`renewal_successful`**).
  * **`failed_payment_date`**: The date of the failed payment (only for **`renewal_failed`**).
  * **`transaction_id`**: The ID of the transaction (varies by event type).
  * **`transaction_amount`**: The payment amount of the transaction(all same)
  * **`transaction_currency`**: The currency of the transaction(all same)
  * `transaction_date`: The date of the transaction (varies by event type).

</details>

***

## **Securing Webhooks with JWT**

All webhooks include a JWT token in the `Authorization` header, allowing you to verify that the event is coming from Flash. You can decode and verify this token using the subscription key provided in the web app.

Example token:

```json
{
  "version": "1.0",
  "eventType": {
    "id": "1",
    "name": "user_signed_up"
  },
  "user_public_key": "55a12716a6c4e8c95fc83dc046c3ea2209d3e3a1b87b15c48ef562b5a8599ed8",
  "exp": "2024-05-01T08:48:38Z"
}
```

**Token Expiration:** The token is valid for 1 hour.

To verify the token, decode it using your subscription key and the `HS256` algorithm.

***

## **Handling Webhooks**

Your webhook should always respond with a `200 OK` status after processing the event. Failure to do so may cause the system to retry the notification.

***

## **Best Practices**

* **Idempotency:** Ensure that your webhook handler can process the same event more than once without causing errors or duplicate actions.
* **Logging:** Implement logging for all webhook events to make troubleshooting easier.

***

## **FAQ**

**Can I set up multiple webhook URLs?**

<details>

<summary><strong>Can I set up multiple webhook URLs?</strong></summary>

Currently, we support one webhook URL per subscription. You can handle different events with routing logic in your app.

</details>

<details>

<summary><strong>What if I stop receiving webhooks?</strong></summary>

Ensure your endpoint is correctly configured and reachable. If the issue persists, contact our support team.

</details>

***

## Conclusion

By integrating our webhook system, you can stay updated on key subscription events in real time, helping automate and optimize your processes. Feel free to contact our developer support team if you need any help.

***

## Webhook Usage - full example

Assuming your webhook URL is `https://webhook` , you can refer to the following code:

{% tabs %}
{% tab title="JavaScript (NodeJS)" %}

```javascript
const jwt = require('jsonwebtoken');

app.post('/webhook', (req, res) => {
	const data = req.body;  // Extract JSON data from the request
	
	const authHeader = req.headers.authorization;
	const token = authHeader && authHeader.split(' ')[1];
	
	if (!token) {
	    return res.status(401).json({ error: 'No token provided' });
	}

	// Verify and decode the JWT token
	const secret = 'your_secret_key';
	jwt.verify(token, secret, (err, decodedToken) => {
	    if (err) {
	        return res.status(401).json({ error: 'Invalid token' });
	    }
	
	    const { version, eventType, user_public_key } = decodedToken;
	
	    // Process the data as needed
	    console.log('User public key:', user_public_key);
	    console.log('Event Type:', eventType);
	});
	
  // Access fields from the JSON payload
  const userPublicKey = data.data.public_key;
  const userName = data.data.name;
  const userEmail = data.data.email;
  
  // Process the data...
	
  res.json({ ok: true });
});

```

{% endtab %}

{% tab title="Python (Flask)" %}

```python
from flask import request, jsonify
import jwt

@app.route('/webhook', methods=['POST'])
def webhook():
    data = request.get_json()

    auth_header = request.headers.get('Authorization')
    token = auth_header.split(" ")[1] if auth_header else None

    if not token:
        return jsonify({'error': 'No token provided'}), 401

    secret = 'your_secret_key'
    try:
        decoded_token = jwt.decode(token, secret, algorithms=['HS256'])
        version = decoded_token.get('version')
        event_type = decoded_token.get('eventType')
        user_public_key = decoded_token.get('user_public_key')

        print(f"User public key: {user_public_key}")
        print(f"Event Type: {event_type}")

    except jwt.ExpiredSignatureError:
        return jsonify({'error': 'Token has expired'}), 401
    except jwt.InvalidTokenError:
        return jsonify({'error': 'Invalid token'}), 401

    user_public_key = data.get('data').get('public_key')
    user_name = data.get('data').get('name')
    user_email = data.get('data').get('email')
    
    return jsonify({'ok': True})

```

{% endtab %}

{% tab title="Ruby (Sinatra)" %}

```ruby
require 'json'
require 'jwt'

post '/webhook' do
  data = JSON.parse(request.body.read)

  auth_header = request.env['HTTP_AUTHORIZATION']
  token = auth_header.split(' ')[1] if auth_header

  halt 401, { error: 'No token provided' }.to_json unless token

  secret = 'your_secret_key'
  begin
    decoded_token = JWT.decode(token, secret, true, algorithm: 'HS256')[0]
    user_public_key = decoded_token['user_public_key']
    event_type = decoded_token['eventType']

    puts "User public key: #{user_public_key}"
    puts "Event Type: #{event_type}"
  
  rescue JWT::ExpiredSignature
    halt 401, { error: 'Token has expired' }.to_json
  rescue JWT::DecodeError
    halt 401, { error: 'Invalid token' }.to_json
  end

  user_public_key = data['data']['public_key']
  user_name = data['data']['name']
  user_email = data['data']['email']
  
  { ok: true }.to_json
end

```

{% endtab %}

{% tab title="Java (Spring Boot)" %}

```java
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import io.jsonwebtoken.*;
import java.util.Map;

@RestController
public class WebhookController {

    @PostMapping("/webhook")
    public ResponseEntity<?> handleWebhook(@RequestBody Map<String, Object> payload, 
                                           @RequestHeader("Authorization") String authHeader) {
        
        String token = authHeader != null ? authHeader.split(" ")[1] : null;
        if (token == null) {
            return ResponseEntity.status(401).body(Map.of("error", "No token provided"));
        }

        String secret = "your_secret_key";
        try {
            Jws<Claims> decodedToken = Jwts.parser()
                                            .setSigningKey(secret)
                                            .parseClaimsJws(token);
            
            String version = (String) decodedToken.getBody().get("version");
            Map<String, Object> eventType = (Map<String, Object>) decodedToken.getBody().get("eventType");
            String userPublicKey = (String) decodedToken.getBody().get("user_public_key");

            System.out.println("User public key: " + userPublicKey);
            System.out.println("Event Type: " + eventType);

        } catch (ExpiredJwtException e) {
            return ResponseEntity.status(401).body(Map.of("error", "Token has expired"));
        } catch (JwtException e) {
            return ResponseEntity.status(401).body(Map.of("error", "Invalid token"));
        }

        Map<String, Object> data = (Map<String, Object>) payload.get("data");
        String userPublicKey = (String) data.get("public_key");
        String userName = (String) data.get("name");
        String userEmail = (String) data.get("email");

        return ResponseEntity.ok(Map.of("ok", true));
    }
}

```

{% endtab %}

{% tab title="PHP" %}

```php
require 'vendor/autoload.php'; // Include JWT library (e.g., firebase/php-jwt)

use \Firebase\JWT\JWT;

$secret = 'your_secret_key';

$data = json_decode(file_get_contents('php://input'), true);

$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? null;
if ($authHeader) {
    $token = explode(' ', $authHeader)[1];
} else {
    http_response_code(401);
    echo json_encode(['error' => 'No token provided']);
    exit;
}

try {
    $decodedToken = JWT::decode($token, $secret, ['HS256']);
    $decodedArray = (array) $decodedToken;

    $userPublicKey = $decodedArray['user_public_key'];
    $eventType = $decodedArray['eventType'];

    echo "User public key: $userPublicKey\n";
    echo "Event Type: $eventType\n";

} catch (Exception $e) {
    http_response_code(401);
    echo json_encode(['error' => 'Invalid token']);
    exit;
}

$userPublicKey = $data['data']['public_key'] ?? null;
$userName = $data['data']['name'] ?? null;
$userEmail = $data['data']['email'] ?? null;

echo json_encode(['ok' => true]);

```

{% endtab %}
{% endtabs %}


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.paywithflash.com/api-documentation/subscriptions/webhooks.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
