ROKIConnect
Connectors Start API SDKs Migrate MCP Portal ES

Salesforce

Salesforce Sales Cloud (Lightning Experience) — Enterprise Edition or higher. Professional Edition does NOT work for the full integration: it lacks Salesforce Sites, which is the only thing that makes receiving the webhook possible.

4 to 8 hours of Setup work, done by the Salesforce administrator or by the consultant. Plus, separately, 1 to 2 hours of a definition meeting with the owner and the accountant, which is where you decide which object the payment gets recorded against and which field is used for reconciliation. That meeting looks like bureaucracy and it's the step that saves the most projects.from the merchant
24 to 40 hours of Apex development for a simple case: payment creation, webhook receiver with signature validation, backup job, unit tests (Salesforce requires 75% code coverage to deploy to production, non-negotiable) and the deployment. If the merchant wants partial payments, multi-currency, approvals, or integrating it with automations that already exist in the org, it climbs to 60-80 hours effortlessly.of development
30 to 50 hours of actual work for a simple case. In real calendar time, count on 3 to 5 weeks from kickoff to the first payment collected in production.in total

Why the calendar is much longer than the hours: in between there are waits that don't depend on anyone in the project. Provisioning the integration license, if none is free, can take days of back and forth with the Salesforce account executive. The sandbox takes time to create or refresh. And the move to production usually waits for the merchant's deployment window, which tends to be end of month or a weekend.

Three things that blow up the estimate, in order of frequency:

1. Discovering in week two that the merchant is on Professional Edition and there is no Salesforce Sites. The whole webhook architecture gets redone. Verify the edition on day one.

2. Discovering there is no invoice object because the merchant has only Sales Cloud, and nobody had decided where to record the payment. Everything stops until the accountant defines it. That's why it's in the prerequisites and not in the middle of the steps.

3. A consultant who starts off following a Connected App tutorial, crashes into the Spring '26 restriction, and loses days figuring out what happened. Show them Step 0 before they start.

And the uncomfortable clarification: none of this includes the cost of the Salesforce licenses, nor of Revenue Cloud Billing if they end up needing the Invoice object. Those conversations are with the Salesforce account executive and can move the project's final number far more than the programming hours.

Who does it

You need the implementation partner, no matter what. There is no way around it.

This is not like connecting a small accounting app. Salesforce ships with no way to call an external API or to receive a webhook without writing Apex code (Salesforce's own programming language). There is no button. There is no free plugin. Someone has to write code.

Concretely:

If the merchant has no Salesforce administrator of their own, the consultant does both parts and the merchant's time gets added to theirs.

An honest warning about the cost: with Salesforce, the cost of the integration isn't in ROKI, it's in the Salesforce consulting hours, which are expensive. A small merchant issuing few invoices per month will probably spend more implementing this than what it saves them. This integration makes sense when Salesforce is already the system the business lives in and there is volume.

What has to exist before you start

1
Confirm the Salesforce edition they have contracted. It has to be Enterprise, Performance, Unlimited or Developer.

Where: "Setup" → in the "Quick Find" box type "Company Information" → "Company Information". Look at the "Organization Edition" field.

Salesforce Sites — the only native mechanism for receiving ROKI's webhook — is available only in "Developer, Enterprise, Performance, and Unlimited Editions" according to the official implementation guide. Professional Edition is left out. If the merchant is on Professional, the integration can still be done, but the webhook has to be solved with an external intermediary (see the webhook section). Verify this BEFORE quoting, not after.

2
Confirm the org has the "API Enabled" permission active.

Where: "Setup" → "Quick Find" → "Profiles" → open the relevant profile → "Administrative Permissions" section → "API Enabled".

Without API Enabled there is no integration of any kind. On Enterprise and above it comes active. On Professional Edition, API access is an add-on paid separately and many merchants don't have it contracted without knowing it.

3
"My Domain" deployed and active.

Where: "Setup" → "Quick Find" → "My Domain".

It's the base of all the org's URLs, including the URL the OAuth token is requested against and the public Site's URL. Salesforce already requires it in all new orgs, but if the org is old it's worth confirming before starting.

4
An available sandbox, preferably of the Developer or Partial Copy type.

Where: "Setup" → "Quick Find" → "Sandboxes" → "New Sandbox".

All development and testing happens here with ROKI's sk_test_ keys. Nobody writes Apex directly in production. Watch out: a sandbox refresh wipes the configuration you made inside it — the External Client App and the certificate have to be redone. Coordinate the refresh calendar before starting.

5
A free "Salesforce Integration" license for the integration user.

Where: "Setup" → "Quick Find" → "Company Information" → look at the "User Licenses" table and find the "Salesforce Integration" row.

It's the cheap, restricted license used to connect systems. According to the Salesforce release note, Enterprise, Unlimited and Performance orgs come with five of these licenses at no additional cost, and Developer Edition with one. I could NOT read that page directly to confirm it word for word (Salesforce's help returned a load error), so confirm it in your own org's table or with the Salesforce account executive before promising it. If none is free, they're purchased as an add-on.

6
Decide which object the payment will be recorded against. This decision is made BEFORE writing a single line of code.

Where: A business decision between the owner, the accountant and the consultant. It materializes in "Setup" → "Quick Find" → "Object Manager".

Here is Salesforce's biggest trap and it has to be said without dressing it up: Sales Cloud does NOT come with an invoice object. The standard "Invoice" object exists on the platform, but the official Revenue Management documentation says verbatim in its "Special Access Rules": "You need the Billing Operations User permission set to access this object" — meaning it belongs to Revenue Cloud Billing, which is a separate product paid separately. If the merchant has only Sales Cloud, there is no invoice to record the payment on and something else has to be chosen. See Step 3 for the options.

7
The Salesforce Sites domain registered.

Where: "Setup" → "Quick Find" → "Sites" → the "Register a Salesforce Sites Domain" step appears there.

It's registered once per org and works for all the Sites you create afterward. It's what provides the public HTTPS URL where ROKI will hit with the webhook. It's an irreversible step in the sense that the chosen subdomain stays fixed — pick a clean corporate name, not a test one.

8
The ROKI Connect keys: sk_test_ for the sandbox and sk_live_ for production, plus the webhook signing secret.

Where: On ROKI's side, not Salesforce's. On the Salesforce side they're stored in a Custom Metadata Type or in a protected record, never written inside the Apex code.

If the key is hardcoded in an Apex class, anyone with permission to view code can read it, and on top of that you have to modify and redeploy code every time it rotates. It's also grounds for automatic rejection if you ever plan to publish on AppExchange.

The steps

  1. Step 0 — Read this before anything else: the "Connected App" tutorials you'll find on Google no longer work

    This is the most important change and almost nobody has internalized it yet.

    For fifteen years, the way to connect anything to Salesforce was to create a "Connected App". Every tutorial, every YouTube video, every blog says that. It's no longer possible.

    Salesforce published the article "New Connected Apps Can No Longer Be Created", which says verbatim: "Starting in the Spring '26 release, the ability to create new legacy Connected Apps will be disabled." Spring '26 has already passed. It is August 2026.

    What it means in practice:

    - The Connected Apps that already existed keep working. Nobody loses service.
    - You can't create a new one. The replacement is called "External Client App" (ECA).
    - There's a temporary escape hatch: you can ask Salesforce Support to enable Connected App creation for you. But the documentation itself warns: "In future releases, Salesforce Support won't be able to enable the creation of new connected apps." It's an extension, not a solution. Don't build the integration on top of that.

    If your consultant quotes the work talking about "creating a Connected App", they're working with outdated information. Ask them directly if they know it's now External Client App. The answer tells you a lot about how up to date they are.

    This entire guide uses External Client App.

  2. Step 1 — Create the integration user

    The principle is simple: ROKI must never enter Salesforce with the owner's user or with an employee's. You create a user that exists solely so the systems can talk to each other.

    Why it truly matters: if tomorrow you fire the employee whose user was used for the integration and deactivate their account, payments silently stop being recorded. Nobody notices until the accountant can't square the month. A dedicated integration user never gets deactivated by accident because it belongs to nobody.

    How it's created:

    1. "Setup" → "Quick Find" → "Users" → "Users" → "New User" button.
    2. In "User License" choose "Salesforce Integration".
    3. In "Profile" choose "Minimum Access - API Only Integrations". That is the exact name of the profile Salesforce creates automatically when provisioning the license, according to the official documentation.
    4. Give it a self-explanatory name, something like "Integracion ROKI Connect", and an email inbox the merchant actually has access to (it's used for alerts and expiration notices).
    5. Save.

    What this license is exactly, put simply: it's a cheap, restricted license, meant for one system to talk to another. The documentation is explicit that "API Only means the user can only access Salesforce via a REST, SOAP, or Bulk API and not through a user interface" — this user can't get in through a screen even if it wanted to, and it adds that "You can't turn off the API-only access granted through this profile". That restriction can't be disabled, and that's exactly what makes it safe.

    Warning: the "Minimum Access - API Only Integrations" profile means "minimum access" for real. Out of the box it can't see or touch anything. All the permissions are granted in the next step, one by one.

  3. Step 2 — Give the integration user the minimum permissions, and only those

    Permissions are NOT granted on the profile. They're granted in a separate permission set, so that exactly what the integration can do stays documented and auditable.

    1. "Setup" → "Quick Find" → "Permission Sets" → "New".
    2. Name it "ROKI Connect Integracion".
    3. Save and go in to edit it.

    What to enable inside:

    - "Object Settings": only the object the payment is recorded against (the one you defined in the prerequisites). "Read" and "Edit" permissions. No "Delete". No "View All" or "Modify All".
    - "Field Permissions" within that object: only the ROKI custom fields you'll create in Step 3, plus the reference field the accountant uses. Don't give it access to the rest of the fields.
    - "Apex Class Access": the Apex classes written in Steps 6 and 7. Without this the code doesn't run under this user.
    - "System Permissions": "API Enabled" has to be active.

    What NOT to give it, and here it pays to be stubborn: do NOT assign it the "Salesforce API Integration" permission set license. The name makes it sound necessary, but Salesforce's documentation says that permission set license "entitles the Salesforce Integration user license with the same user and object permissions available in the System Administrator profile" — that is, it gives your integration user the same permissions as a system administrator. It's the easy way out when something doesn't work and the consultant is in a hurry, and it's exactly the one not to take. If something fails due to permissions, add the specific missing permission to the permission set; don't open everything up.

    4. Go back to the user ("Users") → "Permission Set Assignments" → "Edit Assignments" → assign "ROKI Connect Integracion".

  4. Step 3 — Define where the payment gets recorded (the decision the accountant has to make)

    Here you have to be frank because this is the part that derails the most Salesforce projects.

    Sales Cloud has no invoices. It ships with "Account", "Contact", "Opportunity", "Quote", "Order", "Product". It brings no invoice, no receipt, no payment. Anything that sounds like billing in Salesforce is sold separately.

    Three real scenarios, pick the one that applies:

    SCENARIO A — The merchant has only Sales Cloud (the most common case, by far)

    There is no invoice object. The options:

    - Record against "Opportunity". Custom fields are created on the Opportunity and when the payment comes in it gets marked. It's the cheap option and the one that works this very day. Its limit is conceptual: an Opportunity is a sale in progress, not a fiscal document, and if the merchant collects in several installments the model stretches badly.
    - Create a custom object, for example "Cobro ROKI__c", with lookups to "Account" and "Opportunity". It's more initial work and it's the right thing if there is volume or partial payments. Each payment is its own record and the history stays clean.

    Decide it with the accountant before programming. Migrating from Opportunity to a custom object after six months of loaded data is an entire project, not an adjustment.

    SCENARIO B — The merchant has Revenue Cloud Billing

    There, the "Invoice" objects and the Payments objects do exist. It's the natural place to record the payment. But watch the access: the official documentation for the Invoice object says in "Special Access Rules" that "You need the Billing Operations User permission set to access this object". Meaning the integration user from Step 1 must also be assigned that permission set, and that may consume a license. Confirm it with the account executive before designing on this basis.

    SCENARIO C — The merchant invoices in another system and Salesforce is just the CRM

    It's very common and nobody says it out loud. If the fiscal invoice is issued by another system, putting the payment in Salesforce is informational, not accounting. It's still useful — the salesperson sees they got paid — but it doesn't replace reconciliation. Make it clear with the accountant from day one so nobody expects from Salesforce a report Salesforce won't be able to give.

    The custom fields to create, in any scenario

    "Setup" → "Quick Find" → "Object Manager" → pick the object → "Fields & Relationships" → "New":

    - "ROKI Payment Id" — type "Text(64)", marked as "Unique" and "External ID". Stores the id ROKI returns. Marking it Unique is what prevents a repeated webhook from generating two payments.
    - "ROKI Checkout URL" — type "URL". The link that gets sent to the customer.
    - "ROKI Estado" — type "Picklist" with the values "Pendiente", "Aprobado", "Rechazado".
    - "ROKI Monto" — type "Currency", with two decimals.
    - "ROKI Fecha Acreditacion" — type "Date/Time".

    And the reference field: the external_reference sent to ROKI has to be the number the accountant uses to reconcile. If they reconcile by invoice number, send the invoice number. Don't send Salesforce's internal Id (those 18-character codes like 006Bd00000XXXXX) because the accountant doesn't recognize them, doesn't have them in their chart of accounts and they're of no use to them.

  5. Step 4 — Generate the certificate and create the External Client App

    This is the credential on the Salesforce side. It replaces the old Connected App.

    FIRST the certificate (the developer does it, on their machine, once)

    The JWT flow we're going to use doesn't use a password: it uses a certificate. Salesforce's documentation is explicit: "Salesforce requires that a JWT is signed using RSA SHA256, which uses an uploaded certificate as the signing secret". An X.509 key pair is generated with openssl. The private key stays stored on ROKI's server and never leaves it. The public certificate is uploaded to Salesforce.

    A practical fact that costs dearly to discover late: the certificate has an expiration date. The day it expires, the integration stops working suddenly and with no prior warning. Write the date in the merchant's calendar the same day you generate it, with a thirty-day reminder. It's not paranoia: it's the number one cause of Salesforce integrations that "worked perfectly and one day just stopped".

    THEN the External Client App

    1. "Setup" → "Quick Find" → type "App" → select "App Manager".
    2. "New External Client App" button. Watch out for this: the button is in "App Manager", not in "External Client App Manager". "External Client App Manager" is a different Setup node used to manage the apps already created, not to create them. It's a Salesforce interface inconsistency that wastes everyone's time the first time.
    3. In the "Basic Information" section:
    - App name: "ROKI Connect".
    - "API name": leave the one it suggests on its own (it replaces spaces with underscores).
    - "Contact email": a merchant inbox someone actually reads.
    - "Distribution state": choose "Local". "Local" is for internal use in this org, which is your case. "Packaged" is for distributing the app to other orgs, and that only applies if you go down the AppExchange path.
    4. In the OAuth section check "Enable OAuth".
    5. "Callback URL": the JWT flow doesn't really use it, but the form requires it. Put ROKI's endpoint URL or a consistent placeholder value.
    6. OAuth scopes. In the selector add:
    - "Manage user data via APIs (api)"
    - "Perform requests at any time (refresh_token, offline_access)"
    Don't add more than what's needed. Every extra scope is extra attack surface, and if you ever go to AppExchange they'll ask you about it.
    7. Save with "Save".

    A note about scopes in JWT, so it doesn't surprise you: Salesforce's documentation says that "You can't specify scopes in a JWT bearer token flow; scopes are issued according to the connected app's Permitted Users policy or your organization's API Access Control settings". The actual access the integration will have comes from the permission set in Step 2 and the policy in Step 5, not from what you check here.

  6. Step 5 — Enable the JWT flow and pre-authorize the user

    Without this step, the app exists but rejects every connection attempt with a cryptic error.

    Enable JWT:

    1. "Setup" → "Quick Find" → "External Client App Manager".
    2. Find "ROKI Connect" and in the actions menu choose "Edit Settings".
    3. Go to the "Flow Enablement" section and check "Enable JWT Bearer Flow".
    4. Choose "Upload Files" and upload the X.509 certificate you generated in Step 4.
    5. Save.

    Pre-authorize (this is what almost everyone forgets):

    6. In the same app, go to the policies tab — "Policies".
    7. In "OAuth policies", in the "Permitted Users" dropdown, choose "Admin approved users are pre-authorized".
    8. Save.
    9. Now associate the permission set: in the app's policies section, add the "ROKI Connect Integracion" permission set to the list of selected permission sets.

    Why this is mandatory and not optional: the JWT flow opens no login screen where someone clicks "Allow". Nobody authorizes anything in the moment. Salesforce only issues the token if the authorization was already granted beforehand, and "Admin approved users are pre-authorized" is exactly that. The documentation confirms it: "If your connected app policy is set to 'Admin approved users are pre-authorized,' you can use profiles and permission sets for prior approval when using the JWT bearer flow."

    If this step is left half done, the error you'll see is "Failed: Not Approved for Access" or a bare "invalid_grant". The message says nothing useful. If it shows up, come back here first before reviewing the code.

  7. Step 6 — Test that the token comes out (before writing anything else)

    A control test. Five minutes that save a whole day of blind debugging.

    The developer builds a JWT signed with the private key, with these claims, according to Salesforce's claims documentation:

    - "iss" (Issuer): the Consumer Key of the External Client App.
    - "sub" (Subject): the username of the integration user from Step 1. It's the full username, sandbox suffix included if you're in a sandbox — always forgotten.
    - "aud" (Audience): "Recipient for whom the token is intended". For production it's Salesforce's login endpoint; for sandbox it's the test one. Honest clarification: the Salesforce help page that details the exact aud value for production versus sandbox returned a load error and I couldn't read it verbatim, so confirm the exact value in the official documentation before hardcoding it. Don't guess it.
    - "exp" (Expiration): "Time after which the token expires". Short, on the order of a couple of minutes.

    It's signed with RSA SHA256 and the JWT is POSTed to Salesforce's token endpoint, which is "/services/oauth2/token" on the corresponding login domain.

    If an access_token comes back: the hard half is already solved.
    If "invalid_grant" comes back: the problem is in Step 5 in 90% of cases, or the username in the sub claim is misspelled.

    A security rule Salesforce stresses and that's worth respecting: "always pass sensitive information in the body of a POST request or in a request header — don't use GET parameters in the URL query string". No tokens or keys in the URL, because they end up written in the logs of every server along the way.

  8. Step 7 — Program the payment creation (outbound Apex)

    Developer's work. What the owner needs to understand is what's going to happen on screen.

    The visible behavior: on the Opportunity (or on whichever object they chose) a button appears, say "Cobrar con ROKI". The salesperson presses it and within a few seconds the "ROKI Checkout URL" field fills with a link. That link gets sent to the customer by email or WhatsApp.

    What it does underneath:

    1. Requests the token with the flow from Step 6 and caches it — a new token is not requested on every call.
    2. Makes a POST to https://aura.roki.systems/api/connect/v1/payments with the header Authorization: Bearer sk_live_... (or sk_test_ in sandbox).
    3. Sends the body: { "amount": 1500.00, "external_reference": "FAC-001", "name": "Factura 001" }
    4. Receives the 201 with { id, checkout_url, transaction_id: null } and stores the id in "ROKI Payment Id", the checkout_url in "ROKI Checkout URL", and leaves "ROKI Estado" as "Pendiente".

    Two things that have to be nailed down hard in the code:

    THE AMOUNT GOES IN LEMPIRAS, WITH DECIMALS. One thousand five hundred lempiras is sent as 1500.00. Not as 150000. ROKI doesn't use cents. It's the mistake that costs the most because it doesn't blow up: the payment gets created anyway, the customer sees an amount a hundred times bigger or smaller, and the problem is discovered when someone complains. If the org has multi-currency enabled, double caution: you have to send the amount converted to lempiras, not the value in the record's currency.

    THE MANDATORY PRIOR STEP: ROKI's URL has to be registered in "Setup" → "Quick Find" → "Remote Site Settings" → "New Remote Site", or configured as a "Named Credential". Salesforce blocks every outbound call to a domain that isn't on that allowlist. Without this the code throws an unauthorized callout exception and there is no way it can work.

  9. Step 8 — Program the webhook receiver (inbound Apex REST)

    Developer's work. It's the class ROKI will call when the payment clears.

    You write an Apex class annotated with "@RestResource" and a method annotated with "@HttpPost". That publishes it under the path "/services/apexrest/" followed by the name you gave it.

    What the method has to do, in strict order:

    1. Read the RAW body of the request, exactly as it arrived. In Apex it's accessed with the "requestBody" property, which the official reference defines as type "Blob" and "Returns or sets the body of the request". It's essential to use the raw body, without parsing or re-serializing the JSON: if you convert it to an object and back to text, one space or the order of one key changes and the signature never validates again.
    2. Read the "ROKI-Signature" header from the "headers" property, which is a "Map<String, String>". It comes in the format t=<unix>,v1=<hmac_sha256_hex>.
    3. Recompute the HMAC over timestamp + "." + raw body. In Apex it's done with "Crypto.generateMac" using the HMACSHA256 algorithm, and the result is converted to hexadecimal with "EncodingUtil.convertToHex" so it can be compared with the v1 that arrived.
    4. Compare the two signatures with "areEqualConstantTime", which the documentation describes as the method that "compares two Blobs in constant time in order to avoid timing attack effects". Don't compare with a plain ==: it's a known vulnerability and if you ever go to AppExchange they'll flag it.
    5. Reject old timestamps. If the t= is more than a few minutes old, return an error. This is what prevents someone from capturing a legitimate webhook and resending it a hundred times.
    6. Only now, if the signature validates and the timestamp is fresh, look up the record by "ROKI Payment Id" and, if the event is payment.approved, set "ROKI Estado" to "Aprobado" and fill in "ROKI Fecha Acreditacion".
    7. Be idempotent: if the record is already at "Aprobado", return 200 and do nothing else. ROKI may resend the same event. Without this check, payments get duplicated.
    8. Return 200 fast. All the heavy logic — sending the confirmation email, triggering automations — goes in a separate asynchronous method, not inside the webhook.

    An important detail about the user that runs this: the webhook comes in through the Site from Step 9 and runs under the guest user, not under the integration user. Salesforce's guest user has deliberately very restricted access to records and in general cannot modify records it doesn't own. This surprises experienced developers. The Apex class will probably have to be declared "without sharing" to be able to update the record, and that decision has to be made consciously, with the validation logic locked down tight, because you're running privileged code triggered from the internet.

  10. Step 9 — Publish the endpoint on a Salesforce Site

    This is the step that turns the Apex class into a public URL ROKI can reach.

    1. "Setup" → "Quick Find" → "Sites" → "Sites".
    2. If you haven't done it yet, register the domain in "Register a Salesforce Sites Domain". It's a one-time thing per org and the subdomain stays fixed.
    3. Create a new Site with the "New" button. Name it clearly, something like "ROKI Webhook".
    4. Save.
    5. From the Site's detail page, go into "Public Access Settings". That opens the guest user's profile.
    6. There, enable access to the Apex class from Step 8. The official documentation describes it as "Enable Apex controllers and methods for your site" and in the guest user best practices Salesforce expressly recommends "Allow Apex class access only for REST or SOAP API use" — which is exactly this case.
    7. Give the guest user read and edit permission on the payment object and on the ROKI custom fields. Only those. Nothing else.
    8. Go back to the Site's detail and click "Activate".

    The final URL has the form:
    https://<site-domain>/services/apexrest/<resource-name>

    That's the URL loaded on ROKI's side as the webhook destination. Note that it is NOT the org's normal URL (the My Domain one, the one the employees use). It's the Site's, and it's different. Mixing them up makes ROKI receive a 401 on every attempt and the error doesn't say why.

    Test with curl from outside the merchant's network before calling this step done.

    If the merchant is on Professional Edition: none of this step is available. The Salesforce Sites implementation guide says verbatim "Available in: Developer, Enterprise, Performance, and Unlimited Editions". See the webhook section for the alternative.

  11. Step 10 — The polling backup, which is not optional

    Webhooks get lost. It's a fact of life, not anyone's failure: the Site was under maintenance, the org hit a limit, there was a thirty-second network outage right at that moment. If the only crediting mechanism is the webhook, sooner or later a real payment stays marked as "Pendiente" and the customer has already paid. That's the complaint that ruins trust in the whole system.

    The solution is a scheduled job:

    1. Write an Apex class that implements "Schedulable".
    2. It runs every fifteen or thirty minutes.
    3. It finds all records with "ROKI Estado" = "Pendiente" and "ROKI Payment Id" not empty, created in the last few days.
    4. For each one it queries GET /payments/{id} against ROKI.
    5. If ROKI says approved and Salesforce says pending, it corrects Salesforce.

    It's scheduled from "Setup" → "Quick Find" → "Apex Classes" → "Schedule Apex" button.

    Watch the limits: the Apex REST documentation warns that "Calls to Apex REST classes count against the organization's API governor limits", and outbound calls have their own daily cap. If the merchant handles a lot of volume, the job has to run in batches (Batch Apex) and not in a loose loop, or you'll exhaust the org's callout limit and break other integrations that have nothing to do with this.

  12. Step 11 — Go to production

    1. Test the full circuit in sandbox with sk_test_ keys: create the payment, open the checkout_url, make a test payment, see that the webhook arrives and that the record moves to "Aprobado" on its own.
    2. Test the ugly path: rejected payment, webhook with an invalid signature (it has to reject it), duplicated webhook (it has to be idempotent), and the backup job correcting a payment left pending on purpose.
    3. Deploy the code to production with a Change Set or with a package. You don't copy-paste Apex into production.
    4. In production, redo the Setup configuration from scratch: the External Client App, the certificate, the integration user, the permission set, the Site. None of that travels in a Change Set. Budget these hours; they're always forgotten.
    5. Switch the keys from sk_test_ to sk_live_ and the webhook signing secret.
    6. Load into ROKI the URL of the PRODUCTION Site. It's different from the sandbox one.
    7. First real payment: a small one, of one lempira, with the consultant watching the logs live.
    8. Write the certificate's expiration date in the merchant's calendar, with a thirty-day alert.

  13. Step 12 — About AppExchange: read this before someone sells it to you

    Direct question, direct answer: to connect ROKI to ONE merchant, you don't need AppExchange. At all. Zero.

    AppExchange is Salesforce's app store, and it's for distributing an app to many customers. If you're a merchant integrating your own Salesforce, you install your code in your org and that's it. If someone tells you that "you have to publish on AppExchange" for this to work, they either didn't understand the problem or they're selling you hours.

    Now, if the one reading this is ROKI or a partner who wants to distribute the packaged integration to many merchants, then yes, and it's worth knowing what you're getting into:

    What it requires:
    - Being enrolled in Salesforce's ISV Partner Program. It's a commercial contract with Salesforce, not a form. The documentation lists it as a prerequisite: "Enroll your solution in the ISV Partner Program".
    - The solution being "Lightning Ready". It's mandatory for all new submissions.
    - A Developer Edition org with the solution installed and working test credentials, so Salesforce's security team can go in and review it.
    - Usage documentation and documentation of the data flow between the Salesforce org and everything outside it.
    - Automated scan reports. The documentation is categorical: "if you're listing a managed package, you're required to upload your Salesforce Code Analyzer scan reports", and every false positive has to be justified in writing.

    What it costs:
    - Nine hundred ninety-nine dollars for each security review attempt, for paid solutions. And "for any subsequent attempts" — every retry is paid again. If they reject you twice, you paid three times.
    - For free solutions the documentation says there's no charge for now, "while we work to redefine the policy". It's a policy declared as transitional, meaning it can change.
    - Separately there are the costs of the partner program itself, which are negotiated with Salesforce and aren't public.

    How long it really takes:

    The official documentation breaks the process into stages: "Initial verification: 1–2 weeks", "First review: 3–4 weeks", "Resubmission review: 2–3 weeks". And on another page it summarizes that "A solution typically takes 4–5 weeks to get through the review process".

    Translated into reality: if you pass on the first try, five to six weeks. Almost nobody passes on the first try. Count on two to three months from submission to being published, and that's without counting the time to fix what they flag, which can be quite a lot if the app has code with privileged access — which is precisely the case of a public webhook receiver with "without sharing" Apex.

    And it doesn't end afterward: "Typically, AppExchange solutions are reviewed for security once a year". It's a recurring commitment, not a one-time formality. The good news is that "You can release new package versions without re-submitting them for security review" — you can ship new versions without going back into the queue every time.

    Summary with no runaround: AppExchange is a distribution channel with real cost and a long calendar. It's worth it if you're going to sell the integration to dozens of merchants. To connect one merchant's Salesforce, it's irrelevant.

How the webhook arrives

YES IT CAN, and this is Salesforce's good news compared to ERPs installed at the office.

Salesforce lives in the cloud. There's no server in the back room, no ports to open on the store's router, nothing to ask the internet provider for, no fixed IP to hire. The URL is already on the internet and already has HTTPS with a valid certificate. The whole classic problem of "my system is inside the office and ROKI can't reach it" simply doesn't exist here.

HOW IT'S DONE (the native path)

Two Salesforce pieces are combined:

  1. An Apex class with "@RestResource" and "@HttpPost", which gets published under the path "/services/apexrest/".
  2. A Salesforce Site, which exposes that class to the internet without requiring login, running under the guest user.

The URL you load into ROKI ends up like this: https:///services/apexrest/

Configuration: "Setup" → "Quick Find" → "Sites" → create the Site → "Public Access Settings" → enable the Apex class → back to the Site → "Activate".

THE THREE WARNINGS YOU NEED TO HEAR

First, and it's a dealbreaker: Salesforce Sites does NOT exist in Professional Edition. The official implementation guide says "Available in: Developer, Enterprise, Performance, and Unlimited Editions". If the merchant is on Professional, this path is closed. Verify it before quoting.

Second: you are opening a public door into your Salesforce. An endpoint with no authentication, reachable from anywhere on the planet, that writes records into the business's CRM. It can be done right, but the HMAC signature validation from Step 8 stops being a best practice and becomes the only thing separating your database from the internet. If that validation is loose, anyone who discovers the URL can mark invoices as paid. This is not theoretical.

Third: Salesforce's guest user is deliberately very limited and cannot modify records it doesn't own. Salesforce tightened this security hard after real incidents with misconfigured public sites. The documentation is explicit about the risk: "Guest user sharing rules grant access to guest users without login credentials, allowing immediate and unlimited access to all records matching the sharing rule's criteria to anyone". The code will need to run "without sharing" to be able to update the payment record, and that decision has to be made wide awake, with the validation locked down tight, not copied from a forum.

IF THE NATIVE PATH ISN'T POSSIBLE (Professional Edition, or the merchant's security team forbids public sites)

Alternative: an intermediate relay. A small service outside Salesforce — it can be a twenty-line serverless function — that does three things:

  1. Receives ROKI's webhook and validates the HMAC signature there, outside.
  2. Authenticates against Salesforce with the same JWT flow from Step 6, using the integration user.
  3. Writes to Salesforce through the standard REST API, already authenticated.

Real advantages: Salesforce is never exposed to the internet, Salesforce Sites isn't needed (meaning Professional Edition works), and all writes go through the integration user with its minimal permissions instead of the guest user. Many corporate security teams will prefer this option even if the merchant has Enterprise.

Real cost of the alternative: one more piece of infrastructure to maintain, monitor, and pay for. It's cheaper than it sounds — we're talking cents of a dollar per month at merchant volume — but someone has to be responsible for keeping it alive.

IF ALL ELSE FAILS: the polling backup

Even if the webhook works perfectly, the scheduled job from Step 10 that queries GET /payments/{id} every fifteen or thirty minutes is not optional. A merchant on Professional Edition with no relay can live on this mechanism ALONE: payments get credited with up to half an hour of delay instead of instantly. Not ideal, but it works and it's honestly acceptable for most businesses. Nobody invoices by the second.

What breaks

What we could not confirm

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

Sources