Pay a euro account by SEPA

Check an IBAN, save a SEPA payee and send a EUR payout from a wallet-backed account, converted at the current rate.

Use this recipe to check an IBAN, save a SEPA payee and send a EUR payout from a wallet-backed account. Gravv converts the amount at the current rate.

Who it is for. A US software company that pays a freelance designer's IBAN in Germany.

Status. Available from wallet-backed accounts to the 33 SEPA countries, if Gravv has enabled SEPA payouts for your business. Not supported from bank-held USD accounts.

Money example

  1. The company's wallet-backed USD account holds 5,000.00 USD.
  2. The company sends 1,000.00 USD to Lena Fischer's IBAN by SEPA.
  3. At an example rate of 0.885 EUR per USD, the response shows 885.00 EUR for Lena in local_amount. Gravv debits the account 1,000.00 USD plus the fee on your plan.

Before you start

  • A wallet-backed source account that is active and funded. Bank-held USD accounts cannot pay by SEPA.
  • SEPA payouts enabled for your business. If the first transfer returns transfers to DE/EUR are not supported at this time, contact support.
  • The recipient's IBAN, full name and address, and whether it is a person or a company.

Find out what is required

  • Payee rules. Set payee_type to sepa and currency to EUR, the only currency allowed. Send iban, account_owner_type, bank_name and address. Gravv verifies the IBAN's check digits. bic is optional, and Gravv checks its format when you send it. The IBAN's country must be a SEPA country, and Gravv takes the payee's country from the IBAN.
  • IBAN format. POST /v1/external-accounts/verify checks the format and returns verification_source local. It does not confirm who holds the account.
  • Indicative rate. Call GET /v1/transfer/rates?currency=EUR and read the item with direction USD_EUR. Match on direction, not on currency_pair. The rate is not locked. Gravv prices the transfer when you send it, and local_amount in the transfer response is the amount priced for the recipient.

Steps

1. Check the IBAN (optional)

curl --request POST \
 --url https://api.gravv.xyz/v1/external-accounts/verify \
 --header 'Api-Key: <API_KEY>' \
 --header 'Content-Type: application/json' \
 --data '
{
"iban": "DE89370400440532013000"
}
'
const response = await fetch("https://api.gravv.xyz/v1/external-accounts/verify", {
method: "POST",
headers: {
"Api-Key": process.env.GRAVV_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"iban": "DE89370400440532013000"
}),
});
console.log(response.status, await response.json());
import os

import requests

response = requests.post(
"https://api.gravv.xyz/v1/external-accounts/verify",
headers={
    "Api-Key": os.environ["GRAVV_API_KEY"],
    "Content-Type": "application/json",
},
json={
    "iban": "DE89370400440532013000",
},
)
print(response.status_code, response.json())
{
  "data": {
    "verified": true,
    "verification_source": "local"
  },
  "error": null
}

If a check digit is wrong, the response has verified: false and a message, such as IBAN checksum invalid (mod 97 = 28, expected 1).

2. Save the SEPA payee

curl --request POST \
 --url https://api.gravv.xyz/v1/external-accounts \
 --header 'Api-Key: <API_KEY>' \
 --header 'Content-Type: application/json' \
 --header 'Idempotency-Key: fc5b1691-8679-46d8-b1bf-ae8a8109cc1a' \
 --data '
{
"customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"payee_type": "sepa",
"currency": "EUR",
"account_name": "Lena Fischer",
"iban": "DE89370400440532013000",
"bic": "COBADEFFXXX",
"account_owner_type": "individual",
"bank_name": "Commerzbank",
"address": {
"address_line1": "Torstrasse 101",
"city": "Berlin",
"postal_code": "10119",
"country": "DE"
}
}
'
const response = await fetch("https://api.gravv.xyz/v1/external-accounts", {
method: "POST",
headers: {
"Api-Key": process.env.GRAVV_API_KEY,
"Content-Type": "application/json",
"Idempotency-Key": "fc5b1691-8679-46d8-b1bf-ae8a8109cc1a",
},
body: JSON.stringify({
"customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"payee_type": "sepa",
"currency": "EUR",
"account_name": "Lena Fischer",
"iban": "DE89370400440532013000",
"bic": "COBADEFFXXX",
"account_owner_type": "individual",
"bank_name": "Commerzbank",
"address": {
  "address_line1": "Torstrasse 101",
  "city": "Berlin",
  "postal_code": "10119",
  "country": "DE"
}
}),
});
console.log(response.status, await response.json());
import os

import requests

response = requests.post(
"https://api.gravv.xyz/v1/external-accounts",
headers={
    "Api-Key": os.environ["GRAVV_API_KEY"],
    "Content-Type": "application/json",
    "Idempotency-Key": "fc5b1691-8679-46d8-b1bf-ae8a8109cc1a",
},
json={
    "customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "payee_type": "sepa",
    "currency": "EUR",
    "account_name": "Lena Fischer",
    "iban": "DE89370400440532013000",
    "bic": "COBADEFFXXX",
    "account_owner_type": "individual",
    "bank_name": "Commerzbank",
    "address": {
        "address_line1": "Torstrasse 101",
        "city": "Berlin",
        "postal_code": "10119",
        "country": "DE",
    },
},
)
print(response.status_code, response.json())
{
  "data": {
    "account_name": "LENA FISCHER",
    "account_owner_type": "individual",
    "address": {
      "address_line1": "Torstrasse 101",
      "city": "Berlin",
      "country": "DE",
      "postal_code": "10119"
    },
    "bic": "COBADEFFXXX",
    "currency": "EUR",
    "customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "date_created": "2026-10-13T22:41:24.032709Z",
    "iban": "DE89370400440532013000",
    "id": "0f8fad5b-d9cb-469f-a165-70867728950e",
    "institution_country_iso_code": "DE",
    "institution_name": "Commerzbank",
    "payee_type": "sepa",
    "status": "pending"
  },
  "error": null
}

The payee starts as pending. Wait for the external_account.active webhook before you send the payout.

3. Send the payout

Send amount in the source account's currency.

curl --request POST \
 --url https://api.gravv.xyz/v1/transfer \
 --header 'Api-Key: <API_KEY>' \
 --header 'Content-Type: application/json' \
 --header 'Idempotency-Key: 0f1a9b6f-5e85-427f-b824-5cb46e698d30' \
 --data '
{
"customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"source": {
"source_type": "internal_account",
"id": "9b2d6f0e-3c1a-4e8b-a7d5-2f4c6e8a0b13"
},
"destination": {
"destination_type": "external_account",
"id": "0f8fad5b-d9cb-469f-a165-70867728950e",
"rail": "sepa"
},
"amount": "1000.00",
"description": "Design work October",
"client_reference": "sepa-lf-1013"
}
'
const response = await fetch("https://api.gravv.xyz/v1/transfer", {
method: "POST",
headers: {
"Api-Key": process.env.GRAVV_API_KEY,
"Content-Type": "application/json",
"Idempotency-Key": "0f1a9b6f-5e85-427f-b824-5cb46e698d30",
},
body: JSON.stringify({
"customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"source": {
  "source_type": "internal_account",
  "id": "9b2d6f0e-3c1a-4e8b-a7d5-2f4c6e8a0b13"
},
"destination": {
  "destination_type": "external_account",
  "id": "0f8fad5b-d9cb-469f-a165-70867728950e",
  "rail": "sepa"
},
"amount": "1000.00",
"description": "Design work October",
"client_reference": "sepa-lf-1013"
}),
});
console.log(response.status, await response.json());
import os

import requests

response = requests.post(
"https://api.gravv.xyz/v1/transfer",
headers={
    "Api-Key": os.environ["GRAVV_API_KEY"],
    "Content-Type": "application/json",
    "Idempotency-Key": "0f1a9b6f-5e85-427f-b824-5cb46e698d30",
},
json={
    "customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "source": {
        "source_type": "internal_account",
        "id": "9b2d6f0e-3c1a-4e8b-a7d5-2f4c6e8a0b13",
    },
    "destination": {
        "destination_type": "external_account",
        "id": "0f8fad5b-d9cb-469f-a165-70867728950e",
        "rail": "sepa",
    },
    "amount": "1000.00",
    "description": "Design work October",
    "client_reference": "sepa-lf-1013",
},
)
print(response.status_code, response.json())
{
  "data": {
    "amount": "1000",
    "amount_currency": "USD",
    "reference": "4b7e9d2a-1c3f-4a5b-9e8d-7c6b5a4f3e21",
    "date_created": "2026-10-13T11:00:00.000Z",
    "transfer_status": "pending",
    "transaction_id": "2f9a8b7c-6d5e-4f3a-8b2c-1d0e9f8a7b6c",
    "local_amount": "885",
    "local_currency": "EUR",
    "fee": "2",
    "fee_currency": "USD",
    "requires_approval": false
  },
  "error": null
}

local_amount and local_currency are the amount priced for Lena when Gravv accepted the transfer. If a rate looks wrong (for example exactly 1), stop and contact support before you send more.

4. Follow it to the end

The transaction shows transaction_type global_remittance and channel sepa. The webhooks carry workflow_type sepa_transfer.

What can go wrong

Response Why Fix
400 IBAN checksum invalid (mod 97 = N, expected 1): invalid input or invalid IBAN format The IBAN has a typo Check the IBAN again
400 iban required but missing for sepa payee: invalid input The request has no iban Add iban
400 USD not supported for sepa external accounts: invalid input SEPA payees are EUR only Send "currency": "EUR"
400 required but missing for swift payee: … The IBAN is outside SEPA and the request had no payee_type, so Gravv treated the payee as SWIFT To pay USD abroad, use SWIFT. Gravv does not support EUR outside SEPA
400 this account can only send USD: the recipient must receive USD The source is a bank-held USD account Pay from a wallet-backed account
400 the selected source account cannot be used for an international transfer The source account cannot pay abroad Contact support
400 transfers to DE/EUR are not supported at this time Gravv has not enabled that country for your business Contact support
400 transfer rail "sepa" is not configured SEPA is not set up in this environment Contact support
400 FX rate unavailable for currency conversion No rate is available right now Retry later with a new key. Gravv replays the 400 for 24 hours under the old key
external_account.setup_failed with error_message these account details could not be accepted The details were refused Fix the payee
transfer.status.failed with event_data.transfer_status manual_processing_required A person is checking the payout Do not send it again. Contact support with the reference

Webhooks you will receive

You receive external_account.created, then external_account.active, then several transfer.status.pending events, then transfer.status.completed. Transfer webhooks carry no currency field, so read the amount and currency from the transaction.

Testing in sandbox

Sandbox simulates SEPA payee setup, and the payee becomes active within seconds. verify returns verified: true for DE89370400440532013000 and verified: false for DE89370400440532013001. Sandbox may not offer a wallet-backed source account for every currency. Where it does, fund one with POST /v2/accounts/simulate/deposit.

Reference

Was this page helpful?