Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
456 changes: 235 additions & 221 deletions mintlify/openapi.yaml

Large diffs are not rendered by default.

113 changes: 62 additions & 51 deletions mintlify/snippets/kyc/kyc-webhooks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,52 @@ For regulated platforms, customers are created with `APPROVED` KYC status by def

```json
{
"webhookId": "Webhook:019542f5-b3e7-1d02-0000-000000000020",
"type": "KYC_STATUS",
"timestamp": "2023-07-21T17:32:28Z",
"customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"kycStatus": "APPROVED",
"platformCustomerId": "1234567"
"id": "Webhook:019542f5-b3e7-1d02-0000-000000000020",
"type": "CUSTOMER.KYC_APPROVED",
"timestamp": "2025-07-21T17:32:28Z",
"data": {
"id": "Customer:019542f5-b3e7-1d02-0000-000000000001",
"platformCustomerId": "9f84e0c2a72c4fa",
"customerType": "INDIVIDUAL",
"umaAddress": "[email protected]",
"kycStatus": "APPROVED",
"fullName": "John Michael Doe",
"birthDate": "1990-01-15",
"nationality": "US",
"address": {
"line1": "123 Main Street",
"line2": "Apt 4B",
"city": "San Francisco",
"state": "CA",
"postalCode": "94105",
"country": "US"
},
"createdAt": "2025-07-21T17:32:28Z",
"updatedAt": "2025-07-21T17:32:28Z",
"isDeleted": false
}
}
```

**Webhook Headers:**
- `Content-Type: application/json`
- `X-Webhook-Signature: sha256=abc123...`
- `X-Grid-Signature: {"v": "1", "s": "base64_signature..."}`

<ResponseField name="id" type="string" required>
Unique identifier for this webhook delivery. Use this for idempotency to prevent processing duplicate webhooks.
</ResponseField>

<ResponseField name="customerId" type="string" required>
System-generated unique identifier of the customer whose KYC status has changed.
<ResponseField name="type" type="string" required>
Status-specific event type. KYC webhooks use `CUSTOMER.*` types:
- `CUSTOMER.KYC_APPROVED`: Customer verification completed successfully
- `CUSTOMER.KYC_REJECTED`: Customer verification was rejected
- `CUSTOMER.KYC_SUBMITTED`: KYC verification was initially submitted
- `CUSTOMER.KYC_MANUALLY_APPROVED`: Customer was manually approved by platform
- `CUSTOMER.KYC_MANUALLY_REJECTED`: Customer was manually rejected by platform
</ResponseField>

<ResponseField name="kycStatus" type="string" required>
Final KYC verification status. Webhooks are only sent for final states:
- `APPROVED`: Customer verification completed successfully
- `REJECTED`: Customer verification was rejected
- `EXPIRED`: KYC verification has expired and needs renewal
- `CANCELED`: Verification process was canceled
- `MANUALLY_APPROVED`: Customer was manually approved by platform
- `MANUALLY_REJECTED`: Customer was manually rejected by platform
<ResponseField name="data" type="object" required>
The full customer resource object, same as the corresponding `GET /customers/{id}` endpoint would return. Includes all customer fields such as `id`, `kycStatus`, `fullName`, `birthDate`, `nationality`, `address`, etc.
</ResponseField>

<Note>
Expand All @@ -43,50 +64,40 @@ Intermediate states like `PENDING_REVIEW` do not trigger webhook notifications.
```javascript
// Example webhook handler for KYC status updates
// Note: Only final states trigger webhook notifications
app.post('/webhooks/kyc-status', (req, res) => {
const { customerId, kycStatus } = req.body;
switch (kycStatus) {
case 'APPROVED':
app.post('/webhooks/kyc-status', async (req, res) => {
const { type, data } = req.body;

switch (type) {
case 'CUSTOMER.KYC_APPROVED':
// Activate customer account
await activateCustomer(customerId);
await sendWelcomeEmail(customerId);
await activateCustomer(data.id);
await sendWelcomeEmail(data.id);
break;
case 'REJECTED':

case 'CUSTOMER.KYC_REJECTED':
// Notify support and customer
await notifySupport(customerId, 'KYC_REJECTED');
await sendRejectionEmail(customerId);
await notifySupport(data.id, 'KYC_REJECTED');
await sendRejectionEmail(data.id);
break;
case 'MANUALLY_APPROVED':

case 'CUSTOMER.KYC_MANUALLY_APPROVED':
// Handle manual approval
await activateCustomer(customerId);
await sendWelcomeEmail(customerId);
await activateCustomer(data.id);
await sendWelcomeEmail(data.id);
break;
case 'MANUALLY_REJECTED':

case 'CUSTOMER.KYC_MANUALLY_REJECTED':
// Handle manual rejection
await notifySupport(customerId, 'KYC_MANUALLY_REJECTED');
await sendRejectionEmail(customerId);
break;

case 'EXPIRED':
// Handle expired KYC
await notifyCustomerForReKyc(customerId);
break;

case 'CANCELED':
// Handle canceled verification
await logKycCancelation(customerId);
await notifySupport(data.id, 'KYC_MANUALLY_REJECTED');
await sendRejectionEmail(data.id);
break;

default:
// Log unexpected statuses
console.log(`Unexpected KYC status ${kycStatus} for customer ${customerId}`);
// Log unexpected types
console.log(`Unexpected webhook type ${type} for customer ${data.id}`);
}

res.status(200).send('OK');
});
```
</Accordion>
</Accordion>
60 changes: 30 additions & 30 deletions mintlify/snippets/webhooks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...

app.post('/webhooks/uma', (req, res) => {
const signatureHeader = req.header('X-Grid-Signature');

if (!signatureHeader) {
return res.status(401).json({ error: 'Signature missing' });
}

try {
let signature: Buffer;
try {
Expand All @@ -49,7 +49,7 @@ app.post('/webhooks/uma', (req, res) => {
// If JSON parsing fails, treat as direct base64
signature = Buffer.from(signatureHeader, "base64");
}

// Create verifier with the public key and correct algorithm
const verifier = crypto.createVerify("SHA256");
const payload = await request.text();
Expand All @@ -65,22 +65,22 @@ app.post('/webhooks/uma', (req, res) => {
},
signature,
);

if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}

// Webhook is verified, process it based on type
const webhookData = req.body;
if (webhookData.type === 'INCOMING_PAYMENT') {

if (webhookData.type.startsWith('INCOMING_PAYMENT.')) {
// Process incoming payment webhook
// ...
} else if (webhookData.type === 'OUTGOING_PAYMENT') {
} else if (webhookData.type.startsWith('OUTGOING_PAYMENT.')) {
// Process outgoing payment webhook
// ...
}

// Acknowledge receipt of the webhook
return res.status(200).json({ received: true });
} catch (error) {
Expand Down Expand Up @@ -121,19 +121,19 @@ def handle_webhook():
signature = request.headers.get('X-Grid-Signature')
if not signature:
return jsonify({'error': 'Signature missing'}), 401

try:
# Get the raw request body
request_body = request.get_data()

# Create a SHA-256 hash of the request body
hash_obj = hashes.Hash(hashes.SHA256())
hash_obj.update(request_body)
digest = hash_obj.finalize()

# Decode the base64 signature
signature_bytes = base64.b64decode(signature)

# Verify the signature
try:
public_key.verify(
Expand All @@ -143,19 +143,19 @@ def handle_webhook():
)
except Exception as e:
return jsonify({'error': 'Invalid signature'}), 401

# Webhook is verified, process it based on type
webhook_data = request.json
if webhook_data['type'] == 'INCOMING_PAYMENT':

if webhook_data['type'].startswith('INCOMING_PAYMENT.'):
# Process incoming payment webhook
# ...
pass
elif webhook_data['type'] == 'OUTGOING_PAYMENT':
elif webhook_data['type'].startswith('OUTGOING_PAYMENT.'):
# Process outgoing payment webhook
# ...
pass

# Acknowledge receipt of the webhook
return jsonify({'received': True}), 200
except Exception as e:
Expand All @@ -174,10 +174,10 @@ An example of the test webhook payload is shown below:

```json
{
"test": true,
"timestamp": "2023-08-15T14:32:00Z",
"webhookId": "Webhook:019542f5-b3e7-1d02-0000-000000000007",
"type": "TEST"
"id": "Webhook:019542f5-b3e7-1d02-0000-000000000007",
"type": "TEST",
"timestamp": "2025-08-15T14:32:00Z",
"data": {}
}
```

Expand All @@ -187,7 +187,7 @@ You should verify the signature of the webhook using the Grid public key and the

- **Always verify signatures**: Never process webhooks without verifying their signatures.
- **Use HTTPS**: Ensure your webhook endpoint uses HTTPS to prevent man-in-the-middle attacks.
- **Implement idempotency**: Use the `webhookId` field to prevent processing duplicate webhooks.
- **Implement idempotency**: Use the `id` field to prevent processing duplicate webhooks.
- **Timeout handling**: Implement proper timeout handling and respond to webhooks promptly.

## Retry Policy
Expand All @@ -196,10 +196,10 @@ The Grid API will retry webhooks with the following policy based on the webhook

| Webhook Type | Retry Policy | Notes |
|-------------|-------------|-------|
| TEST | No retries | Used for testing webhook configuration |
| OUTGOING_PAYMENT | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on 409 (duplicate webhooks) |
| INCOMING_PAYMENT | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on: 409 (duplicate webhook) or PENDING status since it is served as an approval mechanism in-flow |
| BULK_UPLOAD | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on 409 (duplicate webhooks) |
| INVITATION_CLAIMED | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on 409 (duplicate webhooks) |
| KYC_STATUS | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on 409 (duplicate webhooks) |
| ACCOUNT_STATUS | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on 409 (duplicate webhooks) |
| `TEST` | No retries | Used for testing webhook configuration |
| `OUTGOING_PAYMENT.*` | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on 409 (duplicate webhooks) |
| `INCOMING_PAYMENT.*` | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on: 409 (duplicate webhook) or PENDING status since it is served as an approval mechanism in-flow |
| `BULK_UPLOAD.*` | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on 409 (duplicate webhooks) |
| `INVITATION.*` | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on 409 (duplicate webhooks) |
| `CUSTOMER.*` | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on 409 (duplicate webhooks) |
| `ACCOUNT.*` | Retry with exponential backoff up to 7 days with maximum interval of 30 mins | No retry on 409 (duplicate webhooks) |
Loading