Skip to main content

Event Types

Card2Crypto sends webhooks for these events:
EventDescriptionWhen It’s Sent
payment.completedPayment was successfulAfter Card2Crypto confirms payment and balance is credited
payment.failedPayment failedWhen Stripe payment fails or is declined
payment.refundedPayment was refundedAfter admin processes a refund

Event Payloads

All webhooks follow the same structure with event-specific differences in the payment.status field.

Common Structure

{
  "event": "payment.completed | payment.failed | payment.refunded",
  "payment": {
    "id": "string",
    "amount": number,
    "currency": "string",
    "status": "string",
    "customer_email": "string",
    "customer_name": "string",
    "metadata": object,
    "created_at": "string",
    "completed_at": "string | null"
  },
  "shop": {
    "id": "string",
    "shop_name": "string"
  },
  "timestamp": "string"
}

payment.completed

Sent when a payment is successfully processed and the seller’s balance is credited.

When It’s Triggered

  1. Customer completes checkout on your white-labeled page
  2. Stripe successfully processes the payment
  3. Card2Crypto credits seller’s balance (amount - 15% fee)
  4. Webhook is sent to your endpoint

Example Payload

{
  "event": "payment.completed",
  "payment": {
    "id": "pay_abc123xyz789",
    "amount": 100.00,
    "currency": "usd",
    "status": "completed",
    "customer_email": "customer@example.com",
    "customer_name": "John Doe",
    "metadata": {
      "order_id": "1234",
      "product": "Premium Plan",
      "custom_field": "any value you want"
    },
    "created_at": "2025-10-16T12:00:00Z",
    "completed_at": "2025-10-16T12:01:30Z"
  },
  "shop": {
    "id": "shop_xyz789",
    "shop_name": "My Store"
  },
  "timestamp": "2025-10-16T12:01:30Z"
}

How to Handle

app.post('/webhooks/card2crypto', async (req, res) => {
  const event = req.body;

  if (event.event === 'payment.completed') {
    const { id, amount, metadata } = event.payment;

    // Update your database
    await db.orders.update({
      where: { id: metadata.order_id },
      data: {
        status: 'paid',
        payment_id: id,
        paid_at: new Date()
      }
    });

    // Grant access to product/service
    await grantAccess(event.payment.customer_email, metadata.product);

    // Send confirmation email
    await sendEmail(event.payment.customer_email, 'Payment confirmed!');
  }

  res.status(200).send('OK');
});

payment.failed

Sent when a payment fails or is declined.

When It’s Triggered

  1. Customer attempts checkout
  2. Stripe declines the payment (insufficient funds, fraud detection, etc.)
  3. Webhook is sent to your endpoint

Example Payload

{
  "event": "payment.failed",
  "payment": {
    "id": "pay_failed_abc123",
    "amount": 100.00,
    "currency": "usd",
    "status": "failed",
    "customer_email": "customer@example.com",
    "customer_name": "John Doe",
    "metadata": {
      "order_id": "1234",
      "product": "Premium Plan"
    },
    "created_at": "2025-10-16T12:00:00Z",
    "completed_at": null
  },
  "shop": {
    "id": "shop_xyz789",
    "shop_name": "My Store"
  },
  "timestamp": "2025-10-16T12:00:15Z"
}

How to Handle

if (event.event === 'payment.failed') {
  const { metadata, customer_email } = event.payment;

  // Update order status
  await db.orders.update({
    where: { id: metadata.order_id },
    data: { status: 'payment_failed' }
  });

  // Notify customer to try again
  await sendEmail(customer_email, 'Payment failed - please try again');

  // Log for analytics
  await analytics.track('payment_failed', {
    order_id: metadata.order_id,
    amount: event.payment.amount
  });
}

payment.refunded

Sent when an admin processes a refund for a completed payment.

When It’s Triggered

  1. Admin approves a refund request in the dashboard
  2. Card2Crypto processes the refund
  3. Seller’s balance is debited
  4. Webhook is sent to your endpoint

Example Payload

{
  "event": "payment.refunded",
  "payment": {
    "id": "pay_abc123xyz789",
    "amount": 100.00,
    "currency": "usd",
    "status": "refunded",
    "customer_email": "customer@example.com",
    "customer_name": "John Doe",
    "metadata": {
      "order_id": "1234",
      "product": "Premium Plan"
    },
    "created_at": "2025-10-16T12:00:00Z",
    "completed_at": "2025-10-16T12:01:30Z"
  },
  "shop": {
    "id": "shop_xyz789",
    "shop_name": "My Store"
  },
  "timestamp": "2025-10-17T15:30:00Z"
}

How to Handle

if (event.event === 'payment.refunded') {
  const { id, metadata, customer_email } = event.payment;

  // Update order status
  await db.orders.update({
    where: { id: metadata.order_id },
    data: {
      status: 'refunded',
      refunded_at: new Date()
    }
  });

  // Revoke access
  await revokeAccess(customer_email, metadata.product);

  // Send refund confirmation
  await sendEmail(customer_email, 'Your payment has been refunded');
}

Field Reference

payment object

FieldTypeDescription
idstringUnique payment identifier (e.g., pay_abc123)
amountnumberPayment amount in USD (e.g., 100.00 = $100.00)
currencystringAlways "usd" (USD is the only supported currency)
statusstringPayment status: completed, failed, or refunded
customer_emailstringCustomer’s email address (optional, can be null)
customer_namestringCustomer’s name (optional, can be null)
metadataobjectCustom data you provided when creating payment
created_atstringISO 8601 timestamp when payment was created
completed_atstring | nullISO 8601 timestamp when payment completed (null for failed payments)

shop object

FieldTypeDescription
idstringYour shop ID
shop_namestringYour shop name

Root fields

FieldTypeDescription
eventstringEvent type: payment.completed, payment.failed, or payment.refunded
timestampstringISO 8601 timestamp when webhook was sent

Handling Multiple Events

Use a switch statement to handle different event types:
app.post('/webhooks/card2crypto', async (req, res) => {
  const event = req.body;

  switch (event.event) {
    case 'payment.completed':
      await handlePaymentCompleted(event.payment);
      break;

    case 'payment.failed':
      await handlePaymentFailed(event.payment);
      break;

    case 'payment.refunded':
      await handlePaymentRefunded(event.payment);
      break;

    default:
      console.log('Unknown event type:', event.event);
  }

  res.status(200).send('OK');
});

async function handlePaymentCompleted(payment) {
  // Grant access, update database, send confirmation
}

async function handlePaymentFailed(payment) {
  // Update order, notify customer
}

async function handlePaymentRefunded(payment) {
  // Revoke access, update database, send refund notice
}

Metadata Usage

The metadata object contains any custom data you provided when creating the payment. This is useful for linking webhooks to your internal systems.

When Creating Payment

const response = await fetch('https://card2crypto.cc/api/v1/payments', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer c2c_live_...',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    amount: 10000, // $100.00
    currency: 'usd',
    return_url: 'https://yoursite.com/success',
    metadata: {
      order_id: '1234',
      user_id: '5678',
      product: 'premium_plan',
      subscription_period: 'monthly'
    }
  })
});

In Webhook

app.post('/webhooks/card2crypto', (req, res) => {
  const { metadata } = req.body.payment;

  // Access your custom data
  const orderId = metadata.order_id;        // "1234"
  const userId = metadata.user_id;          // "5678"
  const product = metadata.product;         // "premium_plan"
  const period = metadata.subscription_period; // "monthly"

  // Update your systems
  await db.orders.update({ where: { id: orderId }, data: { status: 'paid' } });
  await db.users.update({ where: { id: userId }, data: { plan: product } });

  res.status(200).send('OK');
});

Best Practices

1. Always Check Event Type

Don’t assume all webhooks are payment.completed:
// BAD - Assumes all webhooks are completed payments
app.post('/webhooks', (req, res) => {
  grantAccess(req.body.payment.customer_email);
  res.send('OK');
});

// GOOD - Check event type first
app.post('/webhooks', (req, res) => {
  if (req.body.event === 'payment.completed') {
    grantAccess(req.body.payment.customer_email);
  }
  res.send('OK');
});

2. Handle Missing Fields

Not all fields are guaranteed to be present:
const email = event.payment.customer_email || 'no-email-provided';
const name = event.payment.customer_name || 'Anonymous';
const completedAt = event.payment.completed_at || null;

3. Use Metadata Wisely

Store identifiers in metadata to link payments to your system:
// Good metadata
{
  "order_id": "1234",
  "user_id": "5678",
  "product_id": "premium"
}

// Bad metadata - don't store sensitive data
{
  "credit_card": "4242...",     // Never!
  "password": "secret123",       // Never!
  "api_key": "sk_live_..."      // Never!
}

4. Log All Events

Keep records of all webhooks for debugging:
app.post('/webhooks/card2crypto', async (req, res) => {
  // Log webhook
  await db.webhook_logs.create({
    event: req.body.event,
    payment_id: req.body.payment.id,
    received_at: new Date(),
    payload: req.body
  });

  // Process webhook
  handleWebhook(req.body);

  res.status(200).send('OK');
});

Testing Events

Use the “Test Webhook” button in your shop settings to test payment.completed events. For testing other event types in development, you can manually create test payloads:
curl -X POST https://your-dev-server.com/webhooks/card2crypto \
  -H "Content-Type: application/json" \
  -H "X-Card2Crypto-Signature: your_test_signature" \
  -d '{
    "event": "payment.failed",
    "payment": {
      "id": "test_failed_payment",
      "amount": 10.00,
      "currency": "usd",
      "status": "failed",
      "customer_email": "test@example.com",
      "customer_name": "Test User",
      "metadata": { "order_id": "test_123" },
      "created_at": "2025-10-16T12:00:00Z",
      "completed_at": null
    },
    "shop": {
      "id": "shop_test",
      "shop_name": "Test Shop"
    },
    "timestamp": "2025-10-16T12:00:00Z"
  }'

Next Steps

I