Skip to content

Getting Started with RBS Ecosystem APIs

This guide walks through integrating a new app with the RBS ecosystem.


Prerequisites

  1. Ecosystem Organization: Your org must be registered in Colectiva
  2. API Key: Request from RBS admin with appropriate permissions
  3. Webhook Endpoint: URL to receive async notifications

Step 1: Verify Your API Key

Test your API key works:

typescript
const response = await fetch('https://colectiva.redbroomsoftware.com/api/health', {
  headers: {
    'Authorization': `Bearer ${API_KEY}`
  }
});

const { status, org_name, permissions } = await response.json();
console.log(`Connected as: ${org_name}`);
console.log(`Permissions: ${permissions.join(', ')}`);

Step 2: Implement OAuth2 SSO (Camino)

Allow users to sign in with their RBS identity.

2.1 Register Your App

Contact RBS to register your app and receive:

  • client_id
  • client_secret (for confidential clients)
  • Approved redirect_uri

2.2 Initiate Login

typescript
// Generate PKCE values
function generatePKCE() {
  const verifier = crypto.randomBytes(32).toString('base64url');
  const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
  return { verifier, challenge };
}

// Store verifier in session
const { verifier, challenge } = generatePKCE();
session.pkceVerifier = verifier;

// Redirect to Camino
const authUrl = new URL('https://camino.redbroomsoftware.com/oauth/authorize');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', CLIENT_ID);
authUrl.searchParams.set('redirect_uri', 'https://myapp.com/auth/callback');
authUrl.searchParams.set('scope', 'openid profile email');
authUrl.searchParams.set('code_challenge', challenge);
authUrl.searchParams.set('code_challenge_method', 'S256');

redirect(authUrl.toString());

2.3 Handle Callback

typescript
// In /auth/callback handler
export async function GET({ url, cookies }) {
  const code = url.searchParams.get('code');
  const verifier = session.pkceVerifier;

  // Exchange code for tokens
  const tokenResponse = await fetch('https://camino.redbroomsoftware.com/oauth/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      code,
      redirect_uri: 'https://myapp.com/auth/callback',
      client_id: CLIENT_ID,
      code_verifier: verifier
    })
  });

  const { access_token, id_token, refresh_token } = await tokenResponse.json();

  // Decode id_token to get user info
  const payload = JSON.parse(atob(id_token.split('.')[1]));
  const { sub: userId, email, name } = payload;

  // Create session in your app
  await createSession(userId, { email, name, access_token, refresh_token });

  return redirect('/dashboard');
}

Step 3: Accept Payments (Colectiva)

3.1 Create a Payment

typescript
async function createPayment(amount: number, customerEmail: string) {
  const response = await fetch('https://colectiva.redbroomsoftware.com/api/payments', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      amount,
      currency: 'MXN',
      method: 'card',
      description: 'Subscription payment',
      customer: { email: customerEmail },
      return_url: 'https://myapp.com/payment/complete',
      webhook_url: 'https://myapp.com/api/webhooks/payments'
    })
  });

  return response.json();
}

// In your checkout flow
const { checkout_url, payment_id } = await createPayment(500, 'user@example.com');
window.location.href = checkout_url;

3.2 Handle Payment Webhook

typescript
// POST /api/webhooks/payments
export async function POST({ request }) {
  const signature = request.headers.get('x-webhook-signature');
  const body = await request.text();

  // Verify signature
  if (!verifyWebhookSignature(body, signature, WEBHOOK_SECRET)) {
    return new Response('Invalid signature', { status: 401 });
  }

  const event = JSON.parse(body);

  switch (event.event) {
    case 'payment.completed':
      await activateSubscription(event.data.metadata.user_id);
      break;

    case 'payment.failed':
      await notifyPaymentFailed(event.data.customer_email);
      break;
  }

  return new Response('OK');
}

Step 4: Generate Invoices (Constanza)

4.1 Stamp a CFDI for Your Customer

typescript
async function generateInvoice(sale: Sale, customer: Customer) {
  const response = await fetch('https://constanza.redbroomsoftware.com/api/cfdi/stamp', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${API_KEY}`,
      'Content-Type': 'application/json',
      'X-Ecosystem-Org-ID': MY_ORG_ID
    },
    body: JSON.stringify({
      tipo_comprobante: 'I',
      serie: 'V',
      folio: sale.number.toString(),

      receptor: {
        rfc: customer.rfc,
        nombre: customer.legal_name,
        uso_cfdi: customer.uso_cfdi || 'G03',
        domicilio_fiscal_receptor: customer.postal_code,
        regimen_fiscal_receptor: customer.regimen_fiscal
      },

      conceptos: sale.items.map(item => ({
        clave_prod_serv: item.sat_code,
        cantidad: item.quantity,
        clave_unidad: item.unit_code,
        descripcion: item.description,
        valor_unitario: item.unit_price,
        importe: item.quantity * item.unit_price,
        objeto_imp: '02',
        impuestos: {
          traslados: [{
            base: item.quantity * item.unit_price,
            impuesto: '002',
            tipo_factor: 'Tasa',
            tasa_o_cuota: 0.16,
            importe: (item.quantity * item.unit_price) * 0.16
          }]
        }
      }))
    })
  });

  const result = await response.json();

  if (result.success) {
    // Store UUID for reference
    await saveCFDIReference(sale.id, result.uuid, result.pdf_url);
    return result;
  } else {
    throw new Error(`CFDI failed: ${result.error}`);
  }
}

Step 5: Set Up Webhook Handlers

Create a single webhook endpoint that routes to specific handlers:

typescript
// POST /api/webhooks/:source
export async function POST({ request, params }) {
  const { source } = params;
  const signature = request.headers.get('x-webhook-signature');
  const body = await request.text();

  // Get the appropriate secret
  const secret = WEBHOOK_SECRETS[source];
  if (!secret || !verifySignature(body, signature, secret)) {
    return new Response('Unauthorized', { status: 401 });
  }

  const event = JSON.parse(body);

  // Route to handler
  const handlers = {
    colectiva: handleColectivaEvent,
    constanza: handleConstanzaEvent,
    camino: handleCaminoEvent
  };

  await handlers[source]?.(event);

  return new Response('OK');
}

async function handleColectivaEvent(event) {
  switch (event.event) {
    case 'payment.completed':
      // Activate service
      break;
    case 'subscription.cancelled':
      // Deactivate service
      break;
  }
}

async function handleConstanzaEvent(event) {
  switch (event.event) {
    case 'cfdi.stamped':
      // Update sale with CFDI info
      break;
    case 'cfdi.cancelled':
      // Handle cancellation
      break;
  }
}

Step 6: Use AI Services

Classify Expenses

typescript
async function classifyExpenses(transactions: Transaction[]) {
  const response = await fetch('https://constanza.redbroomsoftware.com/api/ai/classify/batch', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${API_KEY}`,
      'Content-Type': 'application/json',
      'X-Ecosystem-Org-ID': MY_ORG_ID
    },
    body: JSON.stringify({
      transactions: transactions.map(t => ({
        id: t.id,
        description: t.description,
        amount: t.amount,
        date: t.date
      }))
    })
  });

  const { results } = await response.json();

  // Update transactions with classifications
  for (const [id, classification] of Object.entries(results)) {
    await updateTransaction(id, {
      category: classification.category,
      account_code: classification.account_code,
      is_deductible: classification.is_deductible
    });
  }
}

Common Integration Patterns

Pattern 1: Payment → Invoice Flow

typescript
// After payment webhook
async function handlePaymentCompleted(payment) {
  // 1. Activate subscription
  await activateSubscription(payment.metadata.subscription_id);

  // 2. Generate invoice if requested
  if (payment.metadata.requires_invoice) {
    const customer = await getCustomer(payment.metadata.customer_id);
    await generateInvoice({
      number: payment.payment_id,
      items: [{ description: payment.description, amount: payment.amount }]
    }, customer);
  }

  // 3. Send confirmation
  await sendEmail(payment.customer_email, 'payment_confirmation', {
    amount: payment.amount,
    date: payment.created_at
  });
}

Pattern 2: Subscription Lifecycle

typescript
// Handle all subscription events
async function handleSubscriptionEvent(event) {
  const { subscription_id, customer_email } = event.data;

  switch (event.event) {
    case 'subscription.created':
      await createUserAccount(customer_email, subscription_id);
      await sendWelcomeEmail(customer_email);
      break;

    case 'subscription.payment_failed':
      await sendPaymentFailedEmail(customer_email);
      await scheduleRetry(subscription_id);
      break;

    case 'subscription.cancelled':
      await scheduleDeactivation(subscription_id, event.data.cancel_at);
      await sendCancellationEmail(customer_email);
      break;
  }
}

Testing

Sandbox Environment

Use these test values in development:

typescript
const TEST_CARDS = {
  success: '4075-5957-1648-3764',
  declined: '4000-0000-0000-0002',
  insufficient: '4000-0000-0000-9995'
};

const TEST_RFC = 'XAXX010101000';  // Generic RFC for testing

Webhook Testing

Use a tool like ngrok to receive webhooks locally:

bash
ngrok http 3000
# Update webhook URLs to use ngrok URL

Troubleshooting

"Invalid API key"

  • Check key hasn't expired
  • Verify permissions match the operation
  • Ensure key is for the correct environment

"Org not found"

  • Verify X-Ecosystem-Org-ID header is correct
  • Check org is properly registered in Colectiva

"CFDI validation failed"

  • Review SAT error codes in response
  • Validate RFC format and existence
  • Check product/service codes are current

Webhook not received

  • Verify URL is publicly accessible (HTTPS required)
  • Check webhook secret is correctly configured
  • Review dead letter queue for failed deliveries

Next Steps

Red Broom Software Ecosystem