MuleSoft

MuleSoft API-Led Connectivity: Architecture and Implementation

DeviDevs Team
10 min read
#mulesoft#api-led#integration-architecture#api-design#microservices

API-led connectivity transforms enterprise integration through layered, reusable APIs. This guide covers architectural patterns, implementation strategies, and governance for successful API-led adoption.

API-Led Architecture Layers

Understanding the three-tier architecture:

┌─────────────────────────────────────────────────────────────────┐
│                    EXPERIENCE LAYER                              │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐       │
│  │ Mobile   │  │   Web    │  │ Partner  │  │   IoT    │       │
│  │   API    │  │   API    │  │   API    │  │   API    │       │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘  └────┬─────┘       │
├───────┼─────────────┼─────────────┼─────────────┼──────────────┤
│       │    PROCESS LAYER          │             │              │
│  ┌────▼─────────────▼─────────────▼─────────────▼────┐         │
│  │              Order Process API                     │         │
│  └────┬─────────────┬─────────────┬─────────────┬────┘         │
│       │             │             │             │               │
│  ┌────▼────┐   ┌────▼────┐   ┌────▼────┐   ┌────▼────┐        │
│  │Customer │   │Inventory│   │ Payment │   │Shipping │        │
│  │ Process │   │ Process │   │ Process │   │ Process │        │
│  └────┬────┘   └────┬────┘   └────┬────┘   └────┬────┘        │
├───────┼─────────────┼─────────────┼─────────────┼──────────────┤
│       │    SYSTEM LAYER           │             │              │
│  ┌────▼────┐   ┌────▼────┐   ┌────▼────┐   ┌────▼────┐        │
│  │Salesforce│  │   SAP   │  │ Stripe  │  │   FedEx  │        │
│  │  sAPI   │   │  sAPI   │  │  sAPI   │  │  sAPI    │        │
│  └────┬────┘   └────┬────┘   └────┬────┘   └────┬────┘        │
└───────┼─────────────┼─────────────┼─────────────┼──────────────┘
        │             │             │             │
   [Salesforce]    [SAP ERP]    [Stripe]      [FedEx API]

System API Implementation

System APIs expose backend systems with standardized interfaces:

Salesforce System API

<!-- salesforce-sapi.xml -->
<mule xmlns="http://www.mulesoft.org/schema/mule/core"
      xmlns:http="http://www.mulesoft.org/schema/mule/http"
      xmlns:salesforce="http://www.mulesoft.org/schema/mule/salesforce"
      xmlns:apikit="http://www.mulesoft.org/schema/mule/mule-apikit">
 
    <!-- API Router -->
    <flow name="salesforce-sapi-main">
        <http:listener config-ref="HTTPS_Listener_Config" path="/api/*">
            <http:response statusCode="#[vars.httpStatus default 200]">
                <http:headers>#[vars.responseHeaders default {}]</http:headers>
            </http:response>
        </http:listener>
 
        <!-- Client ID enforcement -->
        <apikit:router config-ref="salesforce-sapi-config"/>
    </flow>
 
    <!-- GET /customers -->
    <flow name="get:\customers:salesforce-sapi-config">
        <ee:transform doc:name="Build Query">
            <ee:message>
                <ee:set-payload><![CDATA[%dw 2.0
output text/plain
var searchTerm = attributes.queryParams.search default ""
var limit = attributes.queryParams.limit default 100
var offset = attributes.queryParams.offset default 0
---
"SELECT Id, Name, Email, Phone, Industry, BillingCity, BillingCountry,
        CreatedDate, LastModifiedDate
 FROM Account
 WHERE IsDeleted = false" ++
 (if (searchTerm != "") " AND Name LIKE '%" ++ searchTerm ++ "%'" else "") ++
 " ORDER BY Name
 LIMIT " ++ (limit as String) ++
 " OFFSET " ++ (offset as String)]]></ee:set-payload>
            </ee:message>
        </ee:transform>
 
        <salesforce:query config-ref="Salesforce_Config" doc:name="Query Accounts"/>
 
        <ee:transform doc:name="Transform to Canonical">
            <ee:message>
                <ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{
    metadata: {
        totalRecords: sizeOf(payload),
        offset: attributes.queryParams.offset default 0,
        limit: attributes.queryParams.limit default 100,
        source: "salesforce"
    },
    customers: payload map {
        id: $.Id,
        externalId: $.Id,
        name: $.Name,
        email: $.Email,
        phone: $.Phone,
        industry: $.Industry,
        address: {
            city: $.BillingCity,
            country: $.BillingCountry
        },
        createdAt: $.CreatedDate,
        updatedAt: $.LastModifiedDate
    }
}]]></ee:set-payload>
            </ee:message>
        </ee:transform>
    </flow>
 
    <!-- GET /customers/{customerId} -->
    <flow name="get:\customers\(customerId):salesforce-sapi-config">
        <salesforce:query-single config-ref="Salesforce_Config" doc:name="Get Account">
            <salesforce:salesforce-query><![CDATA[
                SELECT Id, Name, Email, Phone, Industry, Website, Description,
                       BillingStreet, BillingCity, BillingState, BillingPostalCode, BillingCountry,
                       ShippingStreet, ShippingCity, ShippingState, ShippingPostalCode, ShippingCountry,
                       NumberOfEmployees, AnnualRevenue, CreatedDate, LastModifiedDate
                FROM Account
                WHERE Id = ':customerId'
            ]]></salesforce:salesforce-query>
            <salesforce:parameters><![CDATA[#[{
                customerId: attributes.uriParams.customerId
            }]]]></salesforce:parameters>
        </salesforce:query-single>
 
        <choice doc:name="Check Result">
            <when expression="#[payload != null]">
                <ee:transform doc:name="Transform Response">
                    <ee:message>
                        <ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{
    id: payload.Id,
    externalId: payload.Id,
    name: payload.Name,
    email: payload.Email,
    phone: payload.Phone,
    website: payload.Website,
    industry: payload.Industry,
    description: payload.Description,
    employees: payload.NumberOfEmployees,
    revenue: payload.AnnualRevenue,
    billingAddress: {
        street: payload.BillingStreet,
        city: payload.BillingCity,
        state: payload.BillingState,
        postalCode: payload.BillingPostalCode,
        country: payload.BillingCountry
    },
    shippingAddress: {
        street: payload.ShippingStreet,
        city: payload.ShippingCity,
        state: payload.ShippingState,
        postalCode: payload.ShippingPostalCode,
        country: payload.ShippingCountry
    },
    createdAt: payload.CreatedDate,
    updatedAt: payload.LastModifiedDate,
    _links: {
        self: "/customers/" ++ payload.Id,
        contacts: "/customers/" ++ payload.Id ++ "/contacts",
        orders: "/customers/" ++ payload.Id ++ "/orders"
    }
}]]></ee:set-payload>
                    </ee:message>
                </ee:transform>
            </when>
            <otherwise>
                <raise-error type="APP:NOT_FOUND" description="Customer not found"/>
            </otherwise>
        </choice>
    </flow>
 
    <!-- POST /customers -->
    <flow name="post:\customers:application\json:salesforce-sapi-config">
        <ee:transform doc:name="Map to Salesforce">
            <ee:message>
                <ee:set-payload><![CDATA[%dw 2.0
output application/java
---
[{
    Name: payload.name,
    Email: payload.email,
    Phone: payload.phone,
    Website: payload.website,
    Industry: payload.industry,
    Description: payload.description,
    NumberOfEmployees: payload.employees,
    BillingStreet: payload.billingAddress.street,
    BillingCity: payload.billingAddress.city,
    BillingState: payload.billingAddress.state,
    BillingPostalCode: payload.billingAddress.postalCode,
    BillingCountry: payload.billingAddress.country
}]]]></ee:set-payload>
            </ee:message>
        </ee:transform>
 
        <salesforce:create config-ref="Salesforce_Config" type="Account" doc:name="Create Account"/>
 
        <ee:transform doc:name="Build Response">
            <ee:message>
                <ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{
    id: payload[0].id,
    success: payload[0].success,
    message: if (payload[0].success) "Customer created successfully"
             else payload[0].errors[0].message
}]]></ee:set-payload>
            </ee:message>
            <ee:variables>
                <ee:set-variable variableName="httpStatus">
                    #[if (payload[0].success) 201 else 400]
                </ee:set-variable>
            </ee:variables>
        </ee:transform>
    </flow>
</mule>

SAP System API

<!-- sap-sapi.xml -->
<flow name="get:\products:sap-sapi-config">
    <ee:transform doc:name="Build RFC Parameters">
        <ee:message>
            <ee:set-payload><![CDATA[%dw 2.0
output application/java
---
{
    I_MATERIAL_TYPE: attributes.queryParams.type default "*",
    I_PLANT: attributes.queryParams.plant default "1000",
    I_MAX_ROWS: attributes.queryParams.limit default 100
}]]></ee:set-payload>
        </ee:message>
    </ee:transform>
 
    <!-- Call SAP RFC/BAPI -->
    <sap:execute-synchronous-remote-function-call
        config-ref="SAP_Config"
        functionName="BAPI_MATERIAL_GETLIST"
        doc:name="Get Materials">
        <sap:content>#[payload]</sap:content>
    </sap:execute-synchronous-remote-function-call>
 
    <ee:transform doc:name="Transform to Canonical">
        <ee:message>
            <ee:set-payload><![CDATA[%dw 2.0
output application/json
 
var materials = payload.MATNRLIST default []
var returnMessages = payload.RETURN default []
---
{
    metadata: {
        totalRecords: sizeOf(materials),
        source: "sap",
        plant: attributes.queryParams.plant default "1000"
    },
    products: materials map {
        id: $.MATERIAL,
        sku: $.MATERIAL,
        name: $.MATL_DESC,
        type: $.MATL_TYPE,
        group: $.MATL_GROUP,
        unit: $.BASE_UOM,
        status: $.MATL_STATUS default "active",
        plant: $.PLANT
    },
    (warnings: returnMessages filter ($.TYPE == "W") map $.MESSAGE)
        if (sizeOf(returnMessages filter ($.TYPE == "W")) > 0)
}]]></ee:set-payload>
        </ee:message>
    </ee:transform>
</flow>
 
<flow name="get:\inventory\{productId}:sap-sapi-config">
    <sap:execute-synchronous-remote-function-call
        config-ref="SAP_Config"
        functionName="BAPI_MATERIAL_STOCK_REQ_LIST"
        doc:name="Get Stock">
        <sap:content><![CDATA[#[{
            MATERIAL: attributes.uriParams.productId,
            PLANT: attributes.queryParams.plant default "1000"
        }]]]></sap:content>
    </sap:execute-synchronous-remote-function-call>
 
    <ee:transform doc:name="Transform Stock Response">
        <ee:message>
            <ee:set-payload><![CDATA[%dw 2.0
output application/json
 
var stockData = payload.STOCK_REQ_LIST default []
---
{
    productId: attributes.uriParams.productId,
    plant: attributes.queryParams.plant default "1000",
    inventory: {
        available: sum(stockData.AVAIL_QTY default [0]),
        reserved: sum(stockData.RES_QTY default [0]),
        blocked: sum(stockData.BLOCK_QTY default [0]),
        inTransit: sum(stockData.TRANSIT_QTY default [0])
    },
    locations: stockData map {
        storageLocation: $.STGE_LOC,
        available: $.AVAIL_QTY,
        reserved: $.RES_QTY,
        unit: $.BASE_UOM
    },
    lastUpdated: now()
}]]></ee:set-payload>
        </ee:message>
    </ee:transform>
</flow>

Process API Implementation

Process APIs orchestrate business processes across system APIs:

<!-- order-process-api.xml -->
<flow name="post:\orders:application\json:order-papi-config">
    <set-variable variableName="orderRequest" value="#[payload]" doc:name="Store Order Request"/>
    <set-variable variableName="orderId" value="#[uuid()]" doc:name="Generate Order ID"/>
 
    <!-- Step 1: Validate Customer -->
    <flow-ref name="validate-customer" doc:name="Validate Customer"/>
 
    <!-- Step 2: Check Inventory -->
    <flow-ref name="check-inventory" doc:name="Check Inventory"/>
 
    <!-- Step 3: Calculate Pricing -->
    <flow-ref name="calculate-pricing" doc:name="Calculate Pricing"/>
 
    <!-- Step 4: Process Payment -->
    <flow-ref name="process-payment" doc:name="Process Payment"/>
 
    <!-- Step 5: Create Order Records -->
    <flow-ref name="create-order-records" doc:name="Create Order Records"/>
 
    <!-- Step 6: Reserve Inventory -->
    <flow-ref name="reserve-inventory" doc:name="Reserve Inventory"/>
 
    <!-- Step 7: Initiate Fulfillment -->
    <flow-ref name="initiate-fulfillment" doc:name="Initiate Fulfillment"/>
 
    <!-- Build Response -->
    <ee:transform doc:name="Build Order Response">
        <ee:message>
            <ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{
    orderId: vars.orderId,
    status: "CONFIRMED",
    customer: vars.customerDetails,
    items: vars.pricedItems,
    totals: vars.orderTotals,
    payment: {
        transactionId: vars.paymentResult.transactionId,
        status: vars.paymentResult.status
    },
    fulfillment: {
        estimatedDelivery: vars.fulfillmentDetails.estimatedDelivery,
        trackingAvailable: vars.fulfillmentDetails.trackingAvailable
    },
    createdAt: now()
}]]></ee:set-payload>
        </ee:message>
    </ee:transform>
 
    <!-- Error Handler with Compensation -->
    <error-handler>
        <on-error-propagate type="ANY">
            <flow-ref name="compensate-order-failure" doc:name="Compensation"/>
        </on-error-propagate>
    </error-handler>
</flow>
 
<sub-flow name="validate-customer">
    <http:request
        method="GET"
        config-ref="Salesforce_SAPI_Config"
        path="/customers/{customerId}"
        doc:name="Get Customer">
        <http:uri-params><![CDATA[#[{
            customerId: vars.orderRequest.customerId
        }]]]></http:uri-params>
    </http:request>
 
    <choice doc:name="Check Customer Status">
        <when expression="#[payload.status == 'SUSPENDED']">
            <raise-error type="APP:CUSTOMER_SUSPENDED" description="Customer account is suspended"/>
        </when>
    </choice>
 
    <set-variable variableName="customerDetails" value="#[payload]" doc:name="Store Customer"/>
</sub-flow>
 
<sub-flow name="check-inventory">
    <scatter-gather doc:name="Check All Items">
        <route>
            <foreach collection="#[vars.orderRequest.items]" doc:name="For Each Item">
                <http:request
                    method="GET"
                    config-ref="SAP_SAPI_Config"
                    path="/inventory/{productId}"
                    doc:name="Check Stock">
                    <http:uri-params><![CDATA[#[{
                        productId: payload.productId
                    }]]]></http:uri-params>
                </http:request>
 
                <choice doc:name="Validate Stock">
                    <when expression="#[payload.inventory.available &lt; vars.rootMessage.payload.quantity]">
                        <raise-error type="APP:INSUFFICIENT_STOCK"
                                     description='Insufficient stock for $(payload.productId)'/>
                    </when>
                </choice>
            </foreach>
        </route>
    </scatter-gather>
 
    <set-variable variableName="inventoryChecked" value="#[true]" doc:name="Mark Inventory Checked"/>
</sub-flow>
 
<sub-flow name="process-payment">
    <http:request
        method="POST"
        config-ref="Stripe_SAPI_Config"
        path="/payments"
        doc:name="Create Payment">
        <http:body><![CDATA[#[{
            amount: vars.orderTotals.grandTotal,
            currency: vars.orderRequest.currency default "USD",
            customerId: vars.customerDetails.stripeCustomerId,
            paymentMethod: vars.orderRequest.payment.methodId,
            description: "Order " ++ vars.orderId,
            metadata: {
                orderId: vars.orderId,
                customerId: vars.orderRequest.customerId
            }
        }]]]></http:body>
    </http:request>
 
    <choice doc:name="Check Payment Result">
        <when expression="#[payload.status != 'succeeded']">
            <raise-error type="APP:PAYMENT_FAILED" description="#[payload.failureMessage]"/>
        </when>
    </choice>
 
    <set-variable variableName="paymentResult" value="#[payload]" doc:name="Store Payment Result"/>
</sub-flow>
 
<sub-flow name="compensate-order-failure">
    <!-- Reverse payment if processed -->
    <choice doc:name="Check Payment to Reverse">
        <when expression="#[vars.paymentResult != null]">
            <http:request
                method="POST"
                config-ref="Stripe_SAPI_Config"
                path="/payments/{paymentId}/refund"
                doc:name="Refund Payment">
                <http:uri-params><![CDATA[#[{
                    paymentId: vars.paymentResult.transactionId
                }]]]></http:uri-params>
            </http:request>
            <logger level="INFO" message="Payment reversed: #[vars.paymentResult.transactionId]"/>
        </when>
    </choice>
 
    <!-- Release inventory if reserved -->
    <choice doc:name="Check Inventory to Release">
        <when expression="#[vars.inventoryReserved == true]">
            <foreach collection="#[vars.orderRequest.items]" doc:name="Release Each Item">
                <http:request
                    method="DELETE"
                    config-ref="SAP_SAPI_Config"
                    path="/inventory/reservations/{reservationId}"
                    doc:name="Release Reservation"/>
            </foreach>
            <logger level="INFO" message="Inventory reservations released"/>
        </when>
    </choice>
 
    <!-- Build error response -->
    <ee:transform doc:name="Build Error Response">
        <ee:message>
            <ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{
    orderId: vars.orderId,
    status: "FAILED",
    error: {
        code: error.errorType.identifier,
        message: error.description,
        compensationApplied: true
    }
}]]></ee:set-payload>
        </ee:message>
    </ee:transform>
</sub-flow>

Experience API Implementation

Experience APIs tailor data for specific consumers:

<!-- mobile-experience-api.xml -->
<flow name="get:\dashboard:mobile-eapi-config">
    <!-- Parallel calls to gather dashboard data -->
    <scatter-gather doc:name="Gather Dashboard Data">
        <!-- Recent Orders -->
        <route>
            <http:request
                method="GET"
                config-ref="Order_PAPI_Config"
                path="/orders"
                doc:name="Get Recent Orders">
                <http:query-params><![CDATA[#[{
                    customerId: vars.userId,
                    limit: 5,
                    status: "RECENT"
                }]]]></http:query-params>
            </http:request>
            <set-variable variableName="recentOrders" value="#[payload]"/>
        </route>
 
        <!-- Account Summary -->
        <route>
            <http:request
                method="GET"
                config-ref="Customer_PAPI_Config"
                path="/customers/{customerId}/summary"
                doc:name="Get Account Summary">
                <http:uri-params><![CDATA[#[{
                    customerId: vars.userId
                }]]]></http:uri-params>
            </http:request>
            <set-variable variableName="accountSummary" value="#[payload]"/>
        </route>
 
        <!-- Notifications -->
        <route>
            <http:request
                method="GET"
                config-ref="Notification_PAPI_Config"
                path="/notifications"
                doc:name="Get Notifications">
                <http:query-params><![CDATA[#[{
                    userId: vars.userId,
                    unreadOnly: true,
                    limit: 10
                }]]]></http:query-params>
            </http:request>
            <set-variable variableName="notifications" value="#[payload]"/>
        </route>
 
        <!-- Recommendations -->
        <route>
            <http:request
                method="GET"
                config-ref="Recommendation_PAPI_Config"
                path="/recommendations"
                doc:name="Get Recommendations">
                <http:query-params><![CDATA[#[{
                    userId: vars.userId,
                    limit: 6
                }]]]></http:query-params>
            </http:request>
            <set-variable variableName="recommendations" value="#[payload]"/>
        </route>
    </scatter-gather>
 
    <!-- Build mobile-optimized response -->
    <ee:transform doc:name="Build Mobile Dashboard">
        <ee:message>
            <ee:set-payload><![CDATA[%dw 2.0
output application/json
 
// Helper to format currency for mobile display
fun formatPrice(amount, currency) =
    currency ++ " " ++ (amount as String {format: "#,##0.00"})
---
{
    user: {
        name: vars.accountSummary.name,
        avatar: vars.accountSummary.avatarUrl,
        memberSince: vars.accountSummary.memberSince as Date as String {format: "MMM yyyy"}
    },
 
    // Compact order cards for mobile
    recentOrders: vars.recentOrders.orders[0 to 2] map {
        id: $.orderId,
        date: $.createdAt as Date as String {format: "MMM d"},
        total: formatPrice($.totals.grandTotal, $.currency),
        status: $.status,
        statusColor: $.status match {
            case "DELIVERED" -> "#4CAF50"
            case "SHIPPED" -> "#2196F3"
            case "PROCESSING" -> "#FF9800"
            else -> "#9E9E9E"
        },
        itemCount: sizeOf($.items),
        thumbnail: $.items[0].imageUrl
    },
 
    // Notification badges
    notifications: {
        unreadCount: sizeOf(vars.notifications.items),
        items: vars.notifications.items[0 to 4] map {
            id: $.id,
            title: $.title,
            preview: if (sizeOf($.message) > 50)
                        $.message[0 to 47] ++ "..."
                     else
                        $.message,
            type: $.type,
            icon: $.type match {
                case "ORDER" -> "shopping_cart"
                case "PROMO" -> "local_offer"
                case "ALERT" -> "warning"
                else -> "notifications"
            },
            timestamp: $.createdAt
        }
    },
 
    // Product cards optimized for mobile grid
    recommendations: vars.recommendations.products map {
        id: $.productId,
        name: if (sizeOf($.name) > 25) $.name[0 to 22] ++ "..." else $.name,
        price: formatPrice($.price, "USD"),
        originalPrice: if ($.onSale) formatPrice($.originalPrice, "USD") else null,
        discount: if ($.onSale) round((1 - $.price / $.originalPrice) * 100) ++ "%" else null,
        image: $.images[0].thumbnailUrl,
        rating: $.rating,
        reviewCount: $.reviewCount
    },
 
    // Quick action buttons
    quickActions: [
        {id: "reorder", label: "Reorder", icon: "replay"},
        {id: "track", label: "Track Order", icon: "local_shipping"},
        {id: "support", label: "Support", icon: "help_outline"},
        {id: "deals", label: "Deals", icon: "local_offer"}
    ],
 
    _meta: {
        generatedAt: now(),
        cacheControl: "max-age=300"
    }
}]]></ee:set-payload>
        </ee:message>
    </ee:transform>
</flow>

API Governance

Establish governance across API layers:

# api-governance-policy.yaml
governance:
  naming_conventions:
    system_api:
      pattern: "{system-name}-sapi"
      examples: ["salesforce-sapi", "sap-sapi", "stripe-sapi"]
    process_api:
      pattern: "{business-process}-papi"
      examples: ["order-papi", "customer-papi", "fulfillment-papi"]
    experience_api:
      pattern: "{channel}-{domain}-eapi"
      examples: ["mobile-commerce-eapi", "web-account-eapi", "partner-orders-eapi"]
 
  versioning:
    strategy: "URI path versioning"
    format: "/v{major}"
    examples:
      - "/v1/customers"
      - "/v2/customers"
    deprecation:
      notice_period_days: 90
      sunset_header: true
 
  security:
    authentication:
      system_api: "Client Credentials OAuth 2.0"
      process_api: "Client Credentials OAuth 2.0"
      experience_api: "Authorization Code OAuth 2.0 or JWT"
    policies:
      required:
        - "Client ID enforcement"
        - "Rate limiting"
        - "Spike control"
      recommended:
        - "IP allowlist (system APIs)"
        - "JWT validation (experience APIs)"
 
  documentation:
    required_elements:
      - "API description and purpose"
      - "Authentication requirements"
      - "Request/response examples"
      - "Error codes and handling"
      - "Rate limits and quotas"
    format: "OAS 3.0"
    hosting: "Anypoint Exchange"
 
  sla:
    system_api:
      availability: "99.5%"
      latency_p95: "500ms"
    process_api:
      availability: "99.5%"
      latency_p95: "2000ms"
    experience_api:
      availability: "99.9%"
      latency_p95: "1000ms"

Conclusion

API-led connectivity provides a scalable, reusable approach to enterprise integration. System APIs abstract backend complexity, Process APIs orchestrate business logic, and Experience APIs optimize for consumer needs. Strong governance ensures consistency and quality across the API landscape. This layered architecture enables agility while maintaining enterprise standards.

Weekly AI Security & Automation Digest

Get the latest on AI Security, workflow automation, secure integrations, and custom platform development delivered weekly.

No spam. Unsubscribe anytime.