# Creating Bc Custom Api

> Creates custom API pages (CRUD) and API queries (read-only joins) in AL for Business Central. Use when exposing custom tables, adding business logic to APIs, creating reporting endpoints, or joining multiple tables for external consumption.

- **Type:** Skill
- **Install:** `agentstack add skill-emgreppi-business-central-ai-skill-creating-bc-custom-api`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [emgreppi](https://agentstack.voostack.com/s/emgreppi)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [emgreppi](https://github.com/emgreppi)
- **Source:** https://github.com/emgreppi/Business-Central-AI-Skill/tree/main/creating-bc-custom-api

## Install

```sh
agentstack add skill-emgreppi-business-central-ai-skill-creating-bc-custom-api
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Skill: Creating BC Custom APIs

## Key Concept

| Type | Object | Operations | Use Case |
|------|--------|------------|----------|
| **API Page** | `PageType = API` | CRUD (Read-Write) | Expose single table |
| **API Query** | `QueryType = API` | Read-only | Join multiple tables |

**For consuming Microsoft standard APIs**, use `al-standard-api` skill instead.

## Validation Gates

1. **After Step 2**: API page compiles, GET returns 200 with fields
2. **After Step 3**: Subpage works, navigation property expands correctly
3. **Final**: All CRUD operations work (GET 200, POST 201, PATCH 200, DELETE 204)

**Note:** OData UI endpoints deprecated BC30 (2027). Always use API pages.

## Placeholder Reference

| Placeholder | Description | Example |
|-------------|-------------|---------|
| `` | Object ID from app.json idRanges | `50100` |
| `` | Company prefix from app.json | `SL`, `ABC` |
| `` | Source table name | `"Rental Machine"` |
| `` | Singular entity name (camelCase) | `rentalMachine` |
| `` | Plural entity name (camelCase) | `rentalMachines` |
| `` | APIPublisher value | `contoso` |
| `` | APIGroup value | `rental` |
| `` | Primary key field | `"No."` |
| ``, `` | Table fields to expose | `"Description"`, `"Status"` |

## Procedure

### Step 1: Decide API Type

**Use API Page when:**
- Need CRUD operations (Create, Read, Update, Delete)
- Exposing a single table
- Need to execute business logic on insert/modify

**Use API Query when:**
- Read-only access sufficient
- Need to join multiple tables
- Reporting/analytics endpoint

### Step 2: Create API Page (CRUD)

```al
page  " API "
{
    APIGroup = '';
    APIPublisher = '';
    APIVersion = 'v2.0';
    EntityName = '';
    EntitySetName = '';
    EntityCaption = '';
    EntitySetCaption = '';
    PageType = API;
    SourceTable = ;
    DelayedInsert = true;
    ODataKeyFields = SystemId;

    layout
    {
        area(Content)
        {
            repeater(General)
            {
                field(id; Rec.SystemId)
                {
                    Caption = 'ID';
                    Editable = false;
                }
                field(; Rec.)
                {
                    Caption = '';
                }
                field(; Rec.)
                {
                    Caption = '';
                }
                field(; Rec.)
                {
                    Caption = '';
                    Editable = false;  // Calculated fields
                }
                field(lastModifiedDateTime; Rec.SystemModifiedAt)
                {
                    Caption = 'Last Modified';
                    Editable = false;
                }
            }
        }
    }

    trigger OnInsertRecord(BelowxRec: Boolean): Boolean
    begin
        // Optional: Set default values or execute business logic
        Rec.Insert(true);
        exit(false);  // Return false to prevent double insert
    end;
}
```

### Step 3: Add Navigation Property (Lines/Details)

For header-line relationships, add a `part` to the parent API page:

```al
layout
{
    area(Content)
    {
        repeater(General)
        {
            // ... header fields ...
        }
        part(Lines; " API  Lines")
        {
            EntityName = 'Line';
            EntitySetName = 'Lines';
            SubPageLink =  = field();
        }
    }
}
```

**Subpage (Lines):**

```al
page  " API  Lines"
{
    APIGroup = '';
    APIPublisher = '';
    APIVersion = 'v2.0';
    EntityName = 'Line';
    EntitySetName = 'Lines';
    PageType = API;
    SourceTable = ;
    DelayedInsert = true;
    ODataKeyFields = SystemId;

    layout
    {
        area(Content)
        {
            repeater(General)
            {
                field(id; Rec.SystemId) { Editable = false; }
                field(; Rec.) { }
                field(lineNo; Rec."Line No.") { }
                // ... other line fields ...
            }
        }
    }
}
```

### Step 4: Create API Query (Read-Only Joins)

```al
query  " API "
{
    QueryType = API;
    APIPublisher = '';
    APIGroup = '';
    APIVersion = 'v2.0';
    EntityName = '';
    EntitySetName = '';

    elements
    {
        dataitem(; )
        {
            column(id; SystemId) { }
            column(; ) { }
            column(; ) { }

            dataitem(; )
            {
                DataItemLink =  = .;
                SqlJoinType = InnerJoin;  // or LeftOuterJoin

                column(lineNo; "Line No.") { }
                column(; ) { }
                column(; ) { }
            }
        }
    }
}
```

## Required Properties

| Property | Required | Description |
|----------|----------|-------------|
| `APIPublisher` | Yes | Your company/publisher name |
| `APIGroup` | Yes | Logical grouping |
| `APIVersion` | Yes | `v2.0` recommended, `beta` for development |
| `EntityName` | Yes | Singular, camelCase |
| `EntitySetName` | Yes | Plural, camelCase |
| `ODataKeyFields` | Yes | Always use `SystemId` |
| `PageType`/`QueryType` | Yes | `API` |
| `DelayedInsert` | Recommended | `true` for API pages |

## API URL Structure

**Custom API endpoint:**
```
https://api.businesscentral.dynamics.com/v2.0///api////companies()/
```

**Example:**
```
https://api.businesscentral.dynamics.com/v2.0/contoso.com/Production/api/contoso/rental/v2.0/companies(xxx)/rentalMachines
```

## Schema Version and OData Features (BC v24+)

### Enum Values (schemaversion 2.0)

Starting BC v24, enum values are returned as XML-encoded names:
- `Return Order` → `Return_x0020_Order`

Add `?$schemaversion=1.0` for old behavior.

### IN Operator (schemaversion 2.1)

```
?$schemaversion=2.1&$filter=status in ('Open', 'Released')
```
Without it → `BadRequest_MethodNotImplemented`.

### Filter inside $expand

```
?$expand=lines($filter=type eq 'Item')
```
Parentheses required around nested options.

### Multi-level Expand

```
?$expand=lines($expand=item($expand=category))
```

## Best Practices

1. **Always use `ODataKeyFields = SystemId`** - stable across renames
2. **Use `EntityCaption`/`EntitySetCaption`** - for localization at `/entityDefinitions`
3. **camelCase for API names** - `rentalMachine` not `RentalMachine`
4. **Alphanumeric only** - no special characters in field names
5. **Version your APIs** - `v1.0`, `v2.0`, or `beta`
6. **Include `SystemModifiedAt`** - enables delta sync with `$filter`
7. **`DelayedInsert = true`** - prevents partial record creation

## CRUD Control

Make API read-only:

```al
page  " API Read Only"
{
    PageType = API;
    InsertAllowed = false;
    ModifyAllowed = false;
    DeleteAllowed = false;
}
```

## Troubleshooting

| Error | Cause | Fix |
|-------|-------|-----|
| 400 | Invalid field name | Check camelCase, no spaces |
| 404 | Entity not found | Verify `ODataKeyFields = SystemId`, entity names |
| 500 | Server error | Check `OnInsertRecord` trigger, BC event log |
| API not visible | Not published | Restart service tier, check compilation |

**Feedback loop:** Fix issue → Recompile → Test GET first → Then test POST/PATCH/DELETE.

## References

See `references/` folder for:
- `api-page-template.al` - Complete AL template for API Page (CRUD), Subpage (Lines), and Read-Only API
- `api-query-template.al` - Complete AL template for API Query with join examples

## External Documentation

- [Microsoft: Developing Custom API](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-develop-custom-api)
- [Microsoft: API Page Type](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-api-pagetype)
- [Microsoft: API Query Type](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-api-querytype)
- [GitHub: APIV2 Examples](https://github.com/microsoft/ALAppExtensions/tree/main/Apps/W1/APIV2/app/src/pages)
- [Kauffmann: Schema Version 2.0](https://www.kauffmann.nl/2024/08/22/custom-apis-and-schemaversion-2-0/)

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [emgreppi](https://github.com/emgreppi)
- **Source:** [emgreppi/Business-Central-AI-Skill](https://github.com/emgreppi/Business-Central-AI-Skill)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-emgreppi-business-central-ai-skill-creating-bc-custom-api
- Seller: https://agentstack.voostack.com/s/emgreppi
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
