Getting Started with RBS Ecosystem APIs
This guide walks through integrating a new app with the RBS ecosystem.
Prerequisites
- Ecosystem Organization: Your org must be registered in Colectiva
- API Key: Request from RBS admin with appropriate permissions
- 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_idclient_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 testingWebhook Testing
Use a tool like ngrok to receive webhooks locally:
bash
ngrok http 3000
# Update webhook URLs to use ngrok URLTroubleshooting
"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-IDheader 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