Apply FX Rates
finance.fx_rate_apply
Overview
You send amounts, a target currency and your own rate table; we convert each amount and report the rate we used, whether it was direct, inverted or routed through your pivot currency, and how old it was. We never fetch, guess or infer a rate. Inversion and triangulation are off until you turn them on, and anything we cannot convert comes back listed with a reason instead of failing the whole batch.
Converting with a rate somebody fetched at an unknown moment, and inverting it quietly when the pair is missing, is how a reporting pack stops matching the treasury's own numbers. You supply the rates; we tell you exactly which one hit each line, how it was derived and how old it was.
Reliability
Rates come only from your table, and resolution is tried in one published order: same currency, then direct, then inverted, then two hops through your pivot. A derived rate is capped at 12 decimals and the value we publish is the value we multiplied by, so you can reproduce every line; when capping changed the rate you get a PRECISION_TRUNCATED warning. FX_RATE_TABLE_INVALID — trigger: an empty table, or an entry with a missing or unsupported currency code, the same currency on both sides, a rate we cannot read, a rate finer than 12 decimals, a malformed asOf date, or a property we do not define; retryable: no, the same table always fails; next: fix the rate at the position we name in the details. FX_RATE_DUPLICATE_PAIR — trigger: two entries give a rate for the same pair, so which one applies would be arbitrary; retryable: no; next: keep one rate per pair per call and split the batch if you genuinely need two. FX_RATE_NON_POSITIVE — trigger: a rate of zero or a negative rate, neither of which is a ratio between currencies; retryable: no; next: send the rate as a positive decimal such as "0.92". FX_PIVOT_UNKNOWN — trigger: a pivot currency we do not support, or one your rate table never mentions, so no amount could be routed through it; retryable: no; next: add the pivot legs to the table, or leave pivotCurrency out. FX_LIMIT_EXCEEDED — trigger: more than 5,000 amounts, or more than 380 rates, which is every ordered pair the 20 supported codes can form; retryable: no; next: send the amounts in batches, reusing the same table. Both limits are ours, not the envelope's: overshoot either by one row or by a thousand and you get this code with the limit and your actual count in the details. CURRENCY_UNKNOWN — trigger: a target currency, or an amount's currency, that is not one of the supported ISO 4217 codes; retryable: no; next: send a supported code. AMOUNT_INVALID — trigger: an empty amounts list, an amount we cannot read as a plain decimal, or one with more than 12 decimals; retryable: no; next: send amounts as plain decimal strings without separators or exponents. AMOUNT_MAGNITUDE_EXCEEDED — trigger: an amount beyond the 1E18 cap we hold exactly; retryable: no; next: scale the batch to thousands and read the result in the same unit.
Context footprint
One row per amount plus a warning per derivation, so a 500-line ledger comes back as a flat list you can page through. A successful call costs 1 execution credit.
When to use it
When you already hold the rates you are contractually meant to use and want them applied consistently across a batch.
When you need current market rates, or a route through several currencies that you have not chosen yourself.
Scope
Non-goals
- Does not fetch, look up or infer a rate from anywhere
- Does not invert or triangulate unless you ask for it
- Does not search for a path longer than two hops
- Does not read the clock — staleness is judged against the date you send
Properties
Capabilities
Cost & billing
Flat rate per invocation
File handling
How we handle your data
What this means
This skill may receive sensitive content and may return submitted, transformed, extracted, or reformatted content.
Squillify guarantees
- Runs inside Squillify
- This skill runs inside Squillify.
- Payloads are not sent to an external service by this skill.
- Squillify does not permanently store submitted payloads.
- Telemetry records metadata only, not submitted content.
- Submitted payloads are not used for training.
- Submitted payloads are not reviewed by humans.
- This skill does not target sensitive data.
- The response may contain submitted or transformed content.
Your responsibility
Squillify does not determine whether submitted content is sensitive. Your application is responsible for deciding what data it is allowed to submit. Your application is responsible for securing the response.
Input schema
| Field | Type | Required | Description |
|---|---|---|---|
| amounts | array | Yes | From 1 through 5,000 closed amounts, each with amount, currency, and an optional join id. |
| targetCurrency | string | Yes | ISO 4217 code every converted figure is expressed in; its minor units set default rounding. |
| rateTable | array | Yes | From 1 through 380 closed directed rates, one entry per from/to pair, covering every ordered pair the 20 supported codes can form. |
| allowInverse | boolean | No | Whether a missing A to B rate may be derived as 1 divided by B to A; omit it for false. |
| pivotCurrency | string | No | ISO 4217 code to route through for at most two hops; omit it and no triangulation happens. |
| asOfLimit | string | No | Date as YYYY-MM-DD; older rates are warned and still applied. Omit it and nothing is judged on age. |
| rounding | object | No | Optional rounding for converted amounts; omit it and the target currency's minor units decide. |
Output schema
| Field | Type | Required | Description |
|---|---|---|---|
| converted | array | Yes | One row per amount that converted, in input order, each with rateUsed, mode, path, and optional asOf. |
| warnings | array | Yes | Derivation and staleness notes, empty when every conversion was a direct untruncated in-date rate. |
| unconverted | array | Yes | Amounts that had no usable rate, each with position, sourceCurrency, and reasonCode; never discards conversions that worked. |
| targetCurrency | string | Yes | ISO 4217 unit every converted figure is expressed in, repeated so a stored conversion names its target. |
| rounding | object | Yes | Mode and scale actually applied to every converted amount after the target currency filled omitted fields. |
API contract
POST /v1/executecurl -sS -X POST "$BASE/v1/execute" -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{"skillKey":"finance.fx_rate_apply","input":{"amounts":[{"id":"A1","amount":"1000.00","currency":"USD"}],"targetCurrency":"EUR","rateTable":[{"from":"USD","to":"EUR","rate":"0.92","asOf":"2026-07-27"}]}}'import { Squillify } from "@squillify/sdk";
const sq = new Squillify({ apiKey: process.env.SQUILLIFY_API_KEY });
const { output } = await sq.execute("finance.fx_rate_apply", {"targetCurrency":"EUR","amounts":[{"id":"A1","amount":"1000.00","currency":"USD"},{"id":"A2","amount":"250.00","currency":"EUR"}],"rateTable":[{"from":"USD","to":"EUR","rate":"0.92","asOf":"2026-07-27"}]});Errors
| Code | HTTP | Retryable | Description |
|---|---|---|---|
| VALIDATION_ERROR | 400 | No | The request payload failed schema validation. |
| UNAUTHORIZED | 401 | No | Missing or invalid API key. |
| SKILL_NOT_ACCESSIBLE | 403 | No | The API key is not granted access to this skill. |
| SKILL_NOT_AVAILABLE_FOR_PLAN | 403 | No | This skill is not included in your current plan. |
| PLAN_ELIGIBILITY_UNAVAILABLE | 503 | Yes | Plan eligibility could not be decided. Retry after backoff. |
| SKILL_NOT_FOUND | 404 | No | The requested skill or version does not exist. |
| TENANT_RATE_LIMITED | 429 | Yes | Request rate limit exceeded. Retry after backoff. |
| QUOTA_EXCEEDED | 429 | Yes | Plan quota exhausted. Retryable once the quota period resets. |
| EXECUTION_TIMEOUT | 504 | Yes | Execution exceeded the allowed time budget. |
| FX_RATE_TABLE_INVALID | 400 | No | The request payload failed this skill's input validation. |
| FX_RATE_DUPLICATE_PAIR | 400 | No | The request payload failed this skill's input validation. |
| FX_RATE_NON_POSITIVE | 400 | No | The request payload failed this skill's input validation. |
| FX_PIVOT_UNKNOWN | 400 | No | The request payload failed this skill's input validation. |
| FX_LIMIT_EXCEEDED | 429 | No | A size, quota, rate, or time limit was exceeded. |
| CURRENCY_UNKNOWN | 400 | No | The request payload failed this skill's input validation. |
| AMOUNT_INVALID | 400 | No | The request payload failed this skill's input validation. |
| AMOUNT_MAGNITUDE_EXCEEDED | 429 | No | A size, quota, rate, or time limit was exceeded. |
Other limits
| Limit | Value |
|---|---|
| Recommended inline bytes | 32 KiB |
Limitations
- Does not fetch, look up or infer a rate from anywhere
- Does not invert or triangulate unless you ask for it
- Does not search for a path longer than two hops
- Does not read the clock — staleness is judged against the date you send
- Live or market rates, which we have no way to obtain
- Rate-path discovery across a graph of currencies
- Deciding whether a rate is too old to use
How to call
POST /v1/execute (requires authentication)
{
"skillKey": "finance.fx_rate_apply",
"input": {
"targetCurrency": "EUR",
"amounts": [
{
"id": "A1",
"amount": "1000.00",
"currency": "USD"
},
{
"id": "A2",
"amount": "250.00",
"currency": "EUR"
}
],
"rateTable": [
{
"from": "USD",
"to": "EUR",
"rate": "0.92",
"asOf": "2026-07-27"
}
]
}
}Examples
A dollar amount converted at your published rate, a euro one passed through
{
"targetCurrency": "EUR",
"amounts": [
{
"id": "A1",
"amount": "1000.00",
"currency": "USD"
},
{
"id": "A2",
"amount": "250.00",
"currency": "EUR"
}
],
"rateTable": [
{
"from": "USD",
"to": "EUR",
"rate": "0.92",
"asOf": "2026-07-27"
}
]
}{
"warnings": [],
"converted": [
{
"id": "A1",
"sourceAmount": "1000",
"sourceCurrency": "USD",
"convertedAmount": "920.00",
"rateUsed": "0.92",
"mode": "DIRECT",
"path": [
"USD",
"EUR"
],
"asOf": "2026-07-27"
},
{
"id": "A2",
"sourceAmount": "250",
"sourceCurrency": "EUR",
"convertedAmount": "250.00",
"rateUsed": "1",
"mode": "IDENTITY",
"path": [
"EUR"
]
}
],
"unconverted": [],
"rounding": {
"scale": 2,
"mode": "HALF_UP"
},
"targetCurrency": "EUR"
}An inverted rate is flagged, and a missing pair is reported not fatal
{
"rateTable": [
{
"from": "USD",
"to": "EUR",
"rate": "0.92"
}
],
"allowInverse": true,
"targetCurrency": "USD",
"amounts": [
{
"id": "B1",
"amount": "500.00",
"currency": "EUR"
},
{
"id": "B2",
"amount": "300.00",
"currency": "GBP"
}
]
}{
"warnings": [
{
"code": "RATE_INVERTED",
"position": 1,
"message": "No direct rate was given for this pair, so we used 1 divided by the reverse rate."
},
{
"code": "PRECISION_TRUNCATED",
"position": 1,
"message": "The rate we derived does not fit in 12 decimals, so we rounded it and converted with the rounded value shown in rateUsed."
}
],
"converted": [
{
"id": "B1",
"sourceAmount": "500",
"sourceCurrency": "EUR",
"convertedAmount": "543.48",
"rateUsed": "1.086956521739",
"mode": "INVERTED",
"path": [
"EUR",
"USD"
]
}
],
"unconverted": [
{
"id": "B2",
"position": 2,
"sourceCurrency": "GBP",
"reasonCode": "NO_RATE_FOR_PAIR"
}
],
"rounding": {
"scale": 2,
"mode": "HALF_UP"
},
"targetCurrency": "USD"
}Recommended use cases
- Restate a multi-currency ledger in your reporting currency at month-end rates
- Convert a supplier's quote using the rate your treasury desk published
- Show a client the exact rate and derivation behind every converted line
Avoid for
- Live or market rates, which we have no way to obtain
- Rate-path discovery across a graph of currencies
- Deciding whether a rate is too old to use
Related skills
Composable with
Safety & privacy
- Runs without calling any external service.
- Your payloads are never stored permanently.
- Telemetry captures metadata only — never your payload contents.
- We never train models on your payloads.
- No human reviews your payloads.