ROKIConnect
Connectors Start API SDKs Migrate MCP Portal ES

QuickBooks

QuickBooks Online (Intuit) — Accounting API v3, OAuth 2.0

3 to 5 hours of real work from the owner and the accountant. It breaks down like this: creating the Intuit developer account and the app, 1 hour. Reviewing and adjusting the QuickBooks configuration (invoice numbering, user permissions, deposit account), 1 to 2 hours with the accountant present. The OAuth authorization click, 10 minutes. Getting or drafting the privacy policy and end user license agreement URLs, 1 to 2 hours (more if they have to be written from scratch). Acceptance testing with small real payments at the end, 1 hour.from the merchant
35 to 55 hours from whoever does the programming, and this is a floor, not an optimistic average. Breakdown: complete OAuth flow with an encrypted token store and automatic renewal with rotation, 10 to 14 hours (it is the longest part and the most underestimated). Webhook receiver with signature validation, replay protection and fast response, 6 to 8 hours. DocNumber to internal Id translation plus creating the Payment with LinkedTxn, 6 to 8 hours. Duplicate control and idempotency, 4 to 6 hours. Fallback task that queries GET /payments/{id}, 3 to 4 hours. Logging, alerts and a minimal panel to see what happened, 4 to 6 hours. Infrastructure (VPS, domain, TLS, deployment), 3 to 5 hours. End to end testing in sandbox, 4 to 6 hours. If the company needs multicurrency, add 8 to 12 more hours.of development
Between 38 and 60 hours of actual work. But on the calendar it is 4 to 8 weeks until the first real payment is recorded.in total

The difference between 60 hours of work and 8 weeks of calendar time is not anyone's laziness: it is Intuit's review. According to Intuit's documentation, the technical review takes on average about 20 days from when it starts, and that timeline depends on their schedule, on how many problems they find, and on how fast you fix them. If they reject your assessment questionnaire, the clock starts over. You can have everything working perfectly in sandbox in two weeks and still wait another month to touch real data.

Two pieces of advice that save weeks: first, submit the assessment questionnaire as soon as you have the connector running in sandbox, not at the end. It is the task with the longest delay and it can run in parallel while you finish the rest. Second, do not promise the owner a launch date until Intuit approves. Promise a date for "ready and tested in sandbox", because that one does depend on you.

And add, on top of all this, between 1 and 3 hours per month of permanent maintenance: checking that token renewal has not broken down, renewing the TLS certificate, and reconciling payments that got stuck.

Who does it

The owner alone canNOT do this. The implementation partner is needed, no way around it.

Split it up like this:

Put bluntly: if there is nobody who can keep a server running, this integration does not hold up. Do not start it.

What has to exist before you start

1
An active QuickBooks Online subscription, and confirming which region/edition the company is on

Where: Gear icon (top right) > "Account and settings" (in the Spanish interface, "Cuenta y configuración") > the "Billing & subscription" / "Facturación y suscripción" tab

Without a live subscription there is no company and therefore no realmId, which is the identifier the API uses to know which company to write to. You also need to check the regional edition: QuickBooks Online is not officially sold in Honduras, and the company's base currency is set when it is created and canNOT be changed later. Find this out BEFORE promising anything, not after.

2
A user with the company administrator role

Where: Gear icon > "Manage users" / "Administrar usuarios"

Only an administrator can authorize a third-party application. A user with a limited role will see the connection screen and it will fail without a clear message as to why. Do not lose half an hour on that.

3
Invoice numbering controlled by you (custom transaction numbers)

Where: Gear icon > "Account and settings" > "Sales" / "Ventas" > "Sales form content" section > "Custom transaction numbers", set to ON

ROKI's external_reference ("FAC-001") has to match the invoice's DocNumber in QuickBooks. If QuickBooks numbers on its own and you do not control that field, there is no way to match the payment to the invoice and the whole integration loses its point.

4
A defined accounting account where the payment lands

Where: Gear icon > "Chart of accounts" / "Plan de cuentas"

In the Payment object this is DepositToAccountRef. If you do not send it, QuickBooks deposits the payment into "Undeposited Funds" ("Fondos no depositados") and someone has to build the deposit by hand, payment by payment. Have the accountant choose it and put it in writing before any programming.

5
The customer already created as a "Customer" in QuickBooks

Where: Side menu "Sales" / "Ventas" > "Customers" / "Clientes"

The Payment object requires CustomerRef. You cannot record a payment against a customer that does not exist. If you sell to occasional end consumers, define a generic customer and always use that one.

6
Multicurrency, ONLY if the company's base currency is not the currency ROKI collects in

Where: Gear icon > "Account and settings" > "Advanced" / "Avanzado" > "Currency" / "Moneda" section > "Multicurrency"

ROKI sends amounts in lempiras in decimal units. If the company has a different base currency, you need CurrencyRef and ExchangeRate on every Payment. Watch out: turning on multicurrency in QuickBooks Online is IRREVERSIBLE. Do not turn it on just to try it.

7
An Intuit developer account, created with an email address belonging to the business

Where: https://developer.intuit.com > "Sign up" / "Sign in"

It is the account that owns the application and the production keys. If the consultant creates it with their personal email, the day they leave, the business loses control of its own integration. Use something like sistemas@tucomercio.com with the password held by the owner.

8
Your own connector server (VPS) with public HTTPS

Where: Outside QuickBooks: it is infrastructure belonging to the business or the implementer

This is the requirement people discover late, and it hurts. You need: port 443/TCP open for inbound traffic from the internet, a valid TLS certificate issued by a public certificate authority (self-signed does NOT work, neither for Intuit nor as the destination for ROKI's webhook), your own domain name, and outbound internet access over 443 to quickbooks.api.intuit.com, oauth.platform.intuit.com and aura.roki.systems. The store where you keep the tokens must be encrypted and have file permissions restricted to the service user (no 0644, and no leaving them in a repository).

The steps

  1. 1. Create the developer account and the application in the Intuit portal

    Go to https://developer.intuit.com and sign up. Then you create the app from the developer dashboard and choose the "QuickBooks Online and Payments" platform.

    Honest warning: Intuit has redesigned and renamed this portal several times in recent years, and its documentation pages are built with JavaScript, so the exact label of every button cannot always be verified. The names that do appear in Intuit's documentation and official support are "My Hub", the "Keys & OAuth" section under "Development" and under "Production" in the sidebar, and the "Keys and credentials" screen. If the button on your screen is called something else, look for the section that shows "Client ID" and "Client Secret": that is the one.

    From the start the app is in development mode, with development keys. That is normal, and that is what you will work with for the first few weeks.

  2. 2. Request the com.intuit.quickbooks.accounting scope and understand that there is no smaller one

    When configuring the app you have to enable the accounting data source (Accounting). That is the com.intuit.quickbooks.accounting scope, and it is the only one you need for this integration: it is the one that lets you read invoices and create the Payment object.

    Here is something you have to tell the owner in plain words: that scope canNOT be narrowed. According to Intuit's list of scopes, there is no read-only permission, none limited to invoices, none limited to payments. com.intuit.quickbooks.accounting gives read and write access to the company's entire accounting: customers, vendors, journal entries, accounts, everything. The principle of "least privilege" cannot be applied here on Intuit's side, because Intuit does not offer it. The only real control is who has the Client Secret and the tokens on your server.

    The com.intuit.quickbooks.payment scope is a different product (Intuit's payment gateway) and you do not need it: ROKI is the one collecting the payment.

  3. 3. Add the Redirect URI

    In your app's "Keys and credentials" section, under "Redirect URIs", you click "Add URI", paste your connector's address, and save with "Save". If you do not save, it does not stick: it is a classic mistake.

    Example: https://conector.tucomercio.com/qbo/callback

    Three rules Intuit does not forgive:
    - It has to be HTTPS. localhost with http is not accepted in production.
    - It has to match, character by character, the one you send in the authorization request. One extra trailing slash and it throws "redirect_uri query parameter value is invalid" at you.
    - Development and Production have SEPARATE lists of Redirect URIs. Adding the development one does not work for production. You can have up to 25 per app.

  4. 4. Create the sandbox company and test there first

    From your developer account you go to "My Hub" > "Sandboxes" and click "Add" (or "Add a sandbox company"), choosing for example QuickBooks Online Plus.

    The sandbox is a fake QuickBooks company, with toy data, so you can break things without fear. There you test with the development keys and against the sandbox base URL.

    Base addresses (confirmed in Intuit's documentation):
    - Sandbox: https://sandbox-quickbooks.api.intuit.com
    - Production: https://quickbooks.api.intuit.com

    The difference between sandbox and production is exactly that: a different base URL, a different Client ID/Client Secret pair, a different list of Redirect URIs, and data that is not real. The OAuth flow is identical and uses the SAME authorization and token endpoints in both cases.

    What the sandbox does NOT test for you: the sandbox is a US company in dollars. If your real company operates in another currency, the entire conversion path remains untested. Keep that in mind.

  5. 5. The OAuth 2.0 flow: the owner's click and where the realmId comes from

    This is the central part and the most misunderstood one. Nobody types a QuickBooks username and password into your system. What happens is this:

    STEP A — your connector sends the QuickBooks administrator to this address (endpoint confirmed in Intuit's OpenID discovery document, https://developer.api.intuit.com/.well-known/openid_configuration):

    https://appcenter.intuit.com/connect/oauth2?client_id=TU_CLIENT_ID&response_type=code&scope=com.intuit.quickbooks.accounting&redirect_uri=https://conector.tucomercio.com/qbo/callback&state=un_valor_aleatorio

    STEP B — the owner sees an Intuit screen, chooses the company, and presses the connect button. That is the only moment a person is involved.

    STEP C — Intuit returns them to your Redirect URI with three things in the address:
    ?code=AB11...&state=un_valor_aleatorio&realmId=9130350000000000

    That realmId is the company's identifier. It is what makes everything work and you have to store it together with the tokens. It goes inside every API call: /v3/company/{realmId}/... . It is not a secret piece of data, but without it you cannot write anywhere. And it is PER COMPANY: if the same consultant connects five businesses, that is five realmId values with five sets of tokens. Do not hardcode it.

    STEP D — your connector exchanges that code for tokens against the token endpoint (also confirmed in the discovery document):

    POST https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer
    Authorization: Basic <client_id:client_secret in base64>
    Content-Type: application/x-www-form-urlencoded
    grant_type=authorization_code&code=AB11...&redirect_uri=https://conector.tucomercio.com/qbo/callback

    And you receive access_token, refresh_token and their expiration times.

    To revoke the connection, the endpoint is https://developer.api.intuit.com/v2/oauth2/tokens/revoke.

  6. 6. Storing and renewing the tokens: this is where most integrations break

    Read this twice, because it is the number one cause of a QuickBooks integration "stopping on its own" after a few months.

    - The access token lasts 1 hour. Nothing more.
    - The refresh token lasts up to 100 days.
    - BUT the refresh token ROTATES: every time you renew, Intuit may return a new refresh token to you, and in practice it does so about once a day. When it returns a new one, the previous one is dead.

    Operating rule: ALWAYS store the last refresh token they returned to you, overwriting the previous one, in the same transaction. If your system holds on to an old one, the next renewal gives you invalid_grant, the connection goes down, and the only way out is for the owner to do the authorization click again by hand.

    And the other ugly scenario: if the business takes a vacation and the connector does not call the API for more than 100 days, the refresh token expires and the connection goes down all the same. A healthy connector renews on its own even when there are no sales.

    Add an alert to the connector: if renewal fails, it should notify someone by email or WhatsApp the same day. Otherwise, they find out when there are already thirty unrecorded payments.

  7. 7. Create the payment in ROKI and send the link to the customer

    When an invoice that will be collected through ROKI is issued in QuickBooks, your connector calls ROKI:

    POST https://aura.roki.systems/api/connect/v1/payments
    Authorization: Bearer sk_live_... (use sk_test_... while you test against the sandbox)
    Content-Type: application/json

    {
    "amount": 1500.00,
    "external_reference": "FAC-001",
    "name": "Factura 001"
    }

    The amount goes in decimal units, in lempiras. NEVER in cents: 1500.00 is one thousand five hundred lempiras, not fifteen lempiras. This is convenient because it matches how QuickBooks handles TotalAmt, which is also decimal. There is no cent conversion anywhere in this integration, and that is how it should be.

    The external_reference has to be the exact DocNumber of the invoice in QuickBooks. It is the thread that will tie the two ends together later on.

    ROKI responds 201 with { id, checkout_url, transaction_id: null }. Store ROKI's id together with the QuickBooks invoice's internal Id in a table of your own: you are going to need it.

    You send the checkout_url to the customer by email or WhatsApp, or you set it as a payment link. That link is what the customer opens to pay.

  8. 8. Receive ROKI's webhook at the connector (not in QuickBooks)

    When the customer pays, ROKI sends a signed notification to a public HTTPS URL. That URL is YOUR connector's, for example https://conector.tucomercio.com/roki/webhook. QuickBooks plays no part in this step: see the point about webhooks further down.

    Validate the signature before believing anything. The ROKI-Signature header carries t=<unix>,v1=<hmac_sha256_hex>. You take the timestamp, append a period, append the RAW BODY exactly as it arrived (without parsing it, without reordering it, without reformatting it), compute HMAC-SHA256 with your secret, and compare. Compare with a constant-time function, not a plain ==. And reject anything with a timestamp older than a few minutes, so nobody can replay an old notification to you.

    If the event is payment.approved, the payment was credited and only then do you proceed.

    Respond quickly with 200 and do the heavy lifting in the background. A webhook that takes too long gets retried, and a badly handled retry duplicates the payment in your books.

  9. 9. Find the invoice's internal Id (the step almost everyone skips)

    Here lies the most costly technical trap in the whole integration, and it is worth understanding even if you do not program.

    In QuickBooks, an invoice has TWO numbers:
    - the DocNumber, which is "FAC-001", the number the customer sees and the one you chose;
    - the Id, which is an internal QuickBooks number like "249", which you do not choose and the customer never sees.

    To record the payment, QuickBooks requires the internal Id. It does not accept the DocNumber. And ROKI gives you back the external_reference, which is the DocNumber. So there is a mandatory translation step:

    GET https://quickbooks.api.intuit.com/v3/company/{realmId}/query?query=select Id, Balance, TotalAmt, CustomerRef from Invoice where DocNumber = 'FAC-001'
    Authorization: Bearer <access_token>
    Accept: application/json

    From there you get the Id, the CustomerRef, and the outstanding Balance.

    Pay attention to this: the QuickBooks query parser sometimes confuses values with hyphens and treats them as dates. A DocNumber like "FAC-001" is right in the risk zone. Test it with your real numbering format before calling the integration done. If it gives you trouble, change the numbering format (for example, without hyphens) while there is still time.

    A safer alternative if you can: store the QuickBooks internal Id in your own table when you create the payment in ROKI (step 7), so in this step you query nothing, you just read your table. It is faster and does not depend on Intuit's parser.

  10. 10. Record the payment against the invoice: the Payment object with Line and LinkedTxn

    Now, the accounting entry itself. In QuickBooks a payment is a Payment object that has a total and a list of lines, and each line says which document it applies to via LinkedTxn. That is the whole logic: the LinkedTxn is the rope that ties the payment to the invoice.

    POST https://quickbooks.api.intuit.com/v3/company/{realmId}/payment?minorversion=<pin a version and leave it pinned>
    Authorization: Bearer <access_token>
    Content-Type: application/json
    Accept: application/json

    {
    "CustomerRef": { "value": "69" },
    "TotalAmt": 1500.00,
    "TxnDate": "2026-08-16",
    "PaymentRefNum": "ROKI-9f3a2",
    "PrivateNote": "ROKI Connect payment, reference FAC-001",
    "DepositToAccountRef": { "value": "35" },
    "Line": [
    {
    "Amount": 1500.00,
    "LinkedTxn": [
    { "TxnId": "249", "TxnType": "Invoice" }
    ]
    }
    ]
    }

    Field by field, in plain English:
    - CustomerRef: the customer. Required, and it has to be the SAME customer as on the invoice.
    - TotalAmt: the total collected, decimal. The 1500.00 that came from ROKI.
    - TxnDate: the date the entry is booked under. Use the date the payment was credited, not the issue date.
    - PaymentRefNum: put ROKI's payment identifier here. It is your audit trail for when the accountant asks where that money came from.
    - PrivateNote: internal note. Leave the external_reference written there. It costs nothing and saves hours.
    - DepositToAccountRef: the account the accountant defined. If you omit it, it goes to "Undeposited Funds" and someone has to deposit it by hand.
    - Line[].Amount: how much of this payment applies to THAT invoice.
    - Line[].LinkedTxn[].TxnId: the invoice's INTERNAL Id (the "249" from the previous step, not "FAC-001").
    - Line[].LinkedTxn[].TxnType: the word "Invoice".

    If a single payment settles several invoices, you add more elements to the Line array, one per invoice, and the sum of the Amount values has to equal TotalAmt.

    Before sending this, check that a Payment with that same PaymentRefNum does not already exist. If the webhook arrives twice and you create two payments, the invoice ends up paid double and you leave the accountant a problem.

  11. 11. Confirm that the invoice was settled

    Do not take a 200 at face value. Query the invoice again and look at the Balance field:

    GET https://quickbooks.api.intuit.com/v3/company/{realmId}/invoice/249

    If Balance ended up at 0, the invoice is settled. If it ended up at anything other than 0, there was a partial payment or something went wrong, and that has to be recorded in your log and, if you can, trigger an alert.

    The invoice will also show you a LinkedTxn array pointing to the Payment: it is the same link seen from the other side. It is useful for auditing.

    Store in your log, for every operation: ROKI's id, the external_reference, the realmId, the invoice's Id, the Id of the Payment created, and the date. The day there is an argument about money, that table is your entire defense.

  12. 12. The fallback for when the webhook does not arrive

    Webhooks get lost. The internet goes down, the server restarts, a certificate expires. Assume it from the design stage, not as a patch.

    Leave a scheduled task that runs every 15 or 30 minutes, takes all the payments you created in ROKI that are still unrecorded in QuickBooks, and queries:

    GET https://aura.roki.systems/api/connect/v1/payments/{id}
    Authorization: Bearer sk_live_...

    If the status is approved and there is no Payment in QuickBooks, you record it with the same procedure as step 10. The "does this payment already exist?" check is what keeps the webhook and this task from creating the payment twice.

    This is not optional. Without this fallback, sooner or later there is a customer who paid and an invoice that shows as unpaid in the books.

  13. 13. Going to production: real keys and Intuit's review

    When everything works in sandbox, you request the production keys in your app's "Production" > "Keys & OAuth" section. Intuit does not hand them over just for asking. First it requires, at a minimum:

    - Completing the app profile.
    - A privacy policy URL and an end user license agreement URL (End User License Agreement). They have to be addresses that actually load. If the app is only for the business's internal use, Intuit support has indicated that a placeholder URL is enough, but if you are going to offer it to third parties you need real documents.
    - Adding the production Redirect URI, which is a separate list from the development one.
    - Completing the app assessment questionnaire. This is mandatory for EVERY app that touches production data, including private ones that are never published in the App Store. Refusing to complete it can end with Intuit cutting off your production access.

    What that questionnaire asks you, so it does not catch you off guard: where the application is hosted, how often you run vulnerability scans, whether you use a security monitoring system (SIEM), what your network security looks like, whether you require multi-factor authentication, how you encrypt data in transit and at rest, how you handle authentication and authorization, and whether you have ISO 27001 certification or a SOC 2 audit.

    Here comes the uncomfortable part, and I would rather tell you now: a small consultant with a shared VPS does not have good answers for half of those questions. Passing is not impossible, but it is not a formality. Prepare it ahead of time.

    And the timelines: according to Intuit's documentation, the technical review takes on average about 20 days from when it starts, and that timeline depends on the review schedule, on how many problems they find, and on how fast you fix them. If they reject you, the clock starts over. Intuit also reviews apps once a year, or more often if it feels like it: this is not a permit you obtain once and are done with.

    Do not give the owner a launch date until Intuit approves.

How the webhook arrives

QuickBooks Online canNOT receive ROKI's webhook. Not with custom development, not by paying more. It does not exist.

It is important to understand why, because the word "webhook" appears in Intuit's documentation and confuses everybody:

QuickBooks Online DOES have webhooks, but they go in the opposite direction. They are notifications that Intuit SENDS OUTWARD, to a URL of yours, when something changes inside QuickBooks (an invoice was created, a customer was modified). They are outbound. QuickBooks exposes no address where a third party like ROKI can knock on the door and say "I collected 1500 lempiras". As a closed cloud system, QuickBooks only accepts writes through its OAuth-authenticated API, never through an anonymous incoming notification.

So, the alternative, which is also the only possible architecture:

THE CONNECTOR IN THE MIDDLE. You need your own service, hosted by the business or by the implementer, with a public HTTPS address. That service:

  1. Receives ROKI's webhook at, for example, https://conector.tucomercio.com/roki/webhook
  2. Validates the ROKI-Signature signature (HMAC-SHA256 over timestamp + "." + raw body)
  3. Responds 200 right away
  4. Translates the external_reference "FAC-001" into the invoice's internal Id in QuickBooks
  5. Calls the QuickBooks API with the current access token and creates the Payment object with its Line/LinkedTxn
  6. Logs everything in its log

What that connector needs, concretely: port 443/TCP open for inbound traffic from the internet, a TLS certificate issued by a public certificate authority (self-signed does not work: ROKI will not deliver the notification to it), your own domain, and outbound access over 443 to quickbooks.api.intuit.com and oauth.platform.intuit.com. No port 80, no bare IP, no homemade tunnels like ngrok for production.

One detail that works in your favor: since QuickBooks Online is a cloud service, your connector can live anywhere with internet. It does not have to be in the business's office, there is no need to open ports on the business's router or expose the internal network. You can put the connector on a VPS and be done. This is the only real advantage QuickBooks Online has over an ERP installed in the office, and it is not a small one.

Now, what nobody tells you: that connector is software that has to be kept running forever. It renews tokens every day, the certificate expires every 90 days, the VPS goes down. If nobody looks after it, the integration dies silently and the business finds out when the accountant closes the month. Define who operates it BEFORE starting, and put it in writing.

And QuickBooks' own webhooks? For this integration you do not need them. They would only be useful if you wanted ROKI to find out when someone issues a new invoice in QuickBooks, to generate the payment automatically. That is a later improvement, not part of the initial rollout. The section where they are configured is inside the app in the developer portal, but I could not confirm the exact label of that tab in the official documentation, so I am not going to make it up for you.

What breaks

What we could not confirm

Every menu name and port on this page was checked against the platform's official documentation. These 7 points could not be confirmed, and are flagged rather than presented as facts:

Sources